Quick Start for Conference in iOS
VideoSDK lets you add video & audio calling to Web, Android and iOS apps, with client SDKs and REST APIs.
This guide will get you running with the VideoSDK video & audio calling in minutes.
Prerequisites
- Xcode, and an iPhone or iPad to run the app on.
App Architecture
This App will contain two screens:
-
Join Screen: This screen lets the user either create a room or join a predefined room. -
Room Screen: This screen contains the local and remote participant views and some room controls such as Enable / Disable Mic & Camera and Leave Room.

Getting Started With the Code!
Step 1: Create App
Step 1: Create a new application by selecting Create a new Xcode project
Step 2: Choose App then click Next

Step 3: Add Product Name and Save the project.

Step 2: VideoSDK Installation
VideoSDK 4.x is installed with Swift Package Manager (SPM). There is no 4.x CocoaPods release.
- Open your Xcode project and go to File > Add Package Dependencies….
- Enter the repository URL:
https://github.com/videosdk-live/videosdk-rtc-ios-spm
- Set Dependency Rule to Exact Version
4.0.0, and add theVideoSDKRTCSwiftproduct to your app target. - Import the library in Swift files:
import VideoSDKRTC
For more details, refer to the official guide on SPM installation.
Then declare the permissions in Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera permission description</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission description</string>
Step 3: Project Structure
iOSQuickStartDemo
├── Models
├── RoomStruct.swift
└── RoomData.swift
├── ViewControllers
├── StartRoomViewController.swift
└── RoomViewController.swift
├── AppDelegate.swift // Default
├── SceneDelegate.swift // Default
└── APIService
└── APIService.swift
├── Main.storyboard // Default
├── LaunchScreen.storyboard // Default
└── Info.plist // Default
Main.storyboard Design

The local and remote video views in Main.storyboard are RTCMTLVideoViews. Select each one, and in the Identity inspector set Class to RTCMTLVideoView and Module to VideoSDKRTC.
Step 4: Create Models
Create Swift files for the RoomData and RoomStruct models.
- Swift
import Foundation
struct RoomData {
let token: String
let name: String
let roomId: String
let micEnabled: Bool
let cameraEnabled: Bool
}
- Swift
import Foundation
struct RoomsStruct: Codable {
let createdAt, updatedAt, roomID: String?
let links: Links?
let id: String?
enum CodingKeys: String, CodingKey {
case createdAt, updatedAt
case roomID = "roomId"
case links, id
}
}
// MARK: - Links
struct Links: Codable {
let getRoom, getSession: String?
enum CodingKeys: String, CodingKey {
case getRoom = "get_room"
case getSession = "get_session"
}
}
Step 5: Get Started with APIClient
First, write an API call that creates a unique room ID. You need an Auth token. Generate it with videosdk-server-api-example or from the Video SDK Dashboard.
- Swift
import Foundation
let TOKEN_STRING: String = "<AUTH_TOKEN>";
class APIService {
class func createRoom(token: String, completion: @escaping (Result<String, Error>) -> Void) {
let url = URL(string: "https://api.videosdk.live/v2/rooms")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue(TOKEN_STRING, forHTTPHeaderField: "authorization")
URLSession.shared.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
DispatchQueue.main.async {
if let data = data, let utf8Text = String(data: data, encoding: .utf8)
{
do{
let dataArray = try JSONDecoder().decode(RoomsStruct.self,from: data)
completion(.success(dataArray.roomID ?? ""))
} catch {
print("Error while creating a room: \(error)")
completion(.failure(error))
}
}
}
}).resume()
}
}
Step 6: Implement Join Screen
The join screen lets the user either create a new room or join an existing room.
- Swift
import Foundation
import UIKit
class StartRoomViewController: UIViewController, UITextFieldDelegate {
private var serverToken = ""
/// MARK: outlet for create room button
@IBOutlet weak var btnCreateRoom: UIButton!
/// MARK: outlet for join room button
@IBOutlet weak var btnJoinRoom: UIButton!
/// MARK: outlet for roomId textfield
@IBOutlet weak var txtRoomId: UITextField!
/// MARK: Initialize the private variable with TOKEN_STRING &
/// setting the room ID in the textfield
override func viewDidLoad() {
txtRoomId.delegate = self
serverToken = TOKEN_STRING
txtRoomId.text = "PROVIDE-STATIC-ROOM-ID"
}
/// MARK: method for joining room through the segue named "StartRoom"
/// after validating the serverToken is not empty
func joinRoom() {
txtRoomId.resignFirstResponder()
if !serverToken.isEmpty {
DispatchQueue.main.async {
self.dismiss(animated: true) {
self.performSegue(withIdentifier: "StartRoom", sender: nil)
}
}
} else {
print("Please provide auth token to start the room.")
}
}
/// MARK: outlet for create room button tap event
@IBAction func btnCreateRoomTapped(_ sender: Any) {
print("show loader while room gets connected with server")
createAndJoinRoom()
}
/// MARK: outlet for join room button tap event
@IBAction func btnJoinRoomTapped(_ sender: Any) {
if((txtRoomId.text ?? "").isEmpty){
print("Please provide room ID to start the room.")
txtRoomId.resignFirstResponder()
} else {
joinRoom()
}
}
// MARK: - method for creating room api call and getting roomId for joining room
func createAndJoinRoom() {
APIService.createRoom(token: self.serverToken) { result in
if case .success(let roomId) = result {
DispatchQueue.main.async {
self.txtRoomId.text = roomId
self.joinRoom()
}
}
}
}
/// MARK: preparing to animate to roomViewController screen
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let navigation = segue.destination as? UINavigationController,
let roomViewController = navigation.topViewController as? RoomViewController else {
return
}
roomViewController.roomData = RoomData(
token: serverToken,
name: "Guest", // your user's display name
roomId: txtRoomId.text ?? "",
micEnabled: true,
cameraEnabled: true
)
}
}
Output

Step 7: Initialize and Join Room
Using the provided token and roomId, we will create the Room in viewDidLoad() with VideoSDK.createRoom(roomId:token:...).
VideoSDK.createRoom(...) throws ERROR_INVALID_PARAMETER for an empty room ID or token, so call it with try. join() is async throws, so call it with try await inside a Task.
Then, we'll add @IBOutlet for localParticipantVideoView and remoteParticipantVideoView, which can render local and remote participant media respectively.
- Swift
import UIKit
import VideoSDKRTC
import AVFoundation
class RoomViewController: UIViewController {
// MARK: - Properties
// outlet for local participant container view
@IBOutlet weak var localParticipantViewContainer: UIView!
// outlet for label for room ID
@IBOutlet weak var lblRoomId: UILabel!
// outlet for local participant video view
@IBOutlet weak var localParticipantVideoView: RTCMTLVideoView!
// outlet for remote participant video view
@IBOutlet weak var remoteParticipantVideoView: RTCMTLVideoView!
// outlet for remote participant no media label
@IBOutlet weak var lblRemoteParticipantNoMedia: UILabel!
// outlet for remote participant container view
@IBOutlet weak var remoteParticipantViewContainer: UIView!
// outlet for local participant no media label
@IBOutlet weak var lblLocalParticipantNoMedia: UILabel!
/// Room data - required to start
var roomData: RoomData!
/// current room reference
private var room: Room?
// MARK: - video participants including self to show in UI
private var participants: [Participant] = []
// MARK: - Lifecycle Events
override func viewDidLoad() {
super.viewDidLoad()
// configure the log level
VideoSDK.setLogLevel(level: .ALL)
// set room ID in button text
lblRoomId.text = "Room ID: \(roomData.roomId)"
// create and join the room
initializeRoom()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.isHidden = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.isHidden = false
NotificationCenter.default.removeObserver(self)
}
// MARK: - Room
private func initializeRoom() {
// Create the room
do {
room = try VideoSDK.createRoom(
roomId: roomData.roomId,
token: roomData.token,
participantName: roomData.name,
micEnabled: roomData.micEnabled,
camEnabled: roomData.cameraEnabled
)
} catch {
print("Setup failed: \(error)") // ERROR_INVALID_PARAMETER for an empty room ID or token
return
}
// Adding the listener to room
room?.addEventListener(self)
// joining the room
Task {
do {
try await room?.join()
} catch {
print("Join failed: \(error)")
}
}
}
}
Step 8: Implement Controls
After initialising the room in the previous step, we will now add @IBOutlet for btnLeave, btnToggleVideo and btnToggleMic, which control the media in the room.
leave(), muteMic(), unmuteMic(), enableCam() and disableCam() are async throws, so each button calls them with try await inside a Task. The mic and camera buttons read room.isMicEnabled and room.isCamEnabled to turn the device off if it is on, and on if it is off.
- Swift
class RoomViewController: UIViewController {
...
// outlet for leave button
@IBOutlet weak var btnLeave: UIButton!
// outlet for toggle video button
@IBOutlet weak var btnToggleVideo: UIButton!
// outlet for toggle audio button
@IBOutlet weak var btnToggleMic: UIButton!
// outlet for leave button click event
@IBAction func btnLeaveTapped(_ sender: Any) {
Task {
do {
try await room?.leave()
} catch {
print("Leave failed: \(error)")
}
dismiss(animated: true)
}
}
// outlet for toggle mic button click event
@IBAction func btnToggleMicTapped(_ sender: Any) {
Task {
do {
guard let room = room else { return }
if room.isMicEnabled {
try await room.muteMic()
} else {
try await room.unmuteMic()
}
} catch let error as VideoSDKError where error == .ERROR_OPERATION_IN_PROGRESS {
// an earlier tap is still running
} catch {
print("Mic toggle failed: \(error)")
}
}
}
// outlet for toggle video button click event
@IBAction func btnToggleVideoTapped(_ sender: Any) {
Task {
do {
guard let room = room else { return }
if room.isCamEnabled {
try await room.disableCam()
} else {
try await room.enableCam()
}
} catch let error as VideoSDKError where error == .ERROR_OPERATION_IN_PROGRESS {
// an earlier tap is still running
} catch {
print("Camera toggle failed: \(error)")
}
}
}
...
}
Output

Step 9: Implementing RoomEventListener
In this step, we'll create an extension for the RoomViewController that implements the RoomEventListener, which implements the onRoomJoined, onRoomLeft, onParticipantJoined, onParticipantLeft, onSpeakerChanged, onError, etc. methods.
onError(error:) receives errors that the SDK reports on its own, such as a camera or microphone that could not be opened. Most failures of your own calls are thrown, so the code in this guide handles them in catch blocks.
- Swift
class RoomViewController: UIViewController {
//...
}
extension RoomViewController: RoomEventListener {
/// Room joined
func onRoomJoined() {
// handle local participant on start
guard let localParticipant = self.room?.localParticipant else { return }
// add to list
participants.append(localParticipant)
// add event listener
localParticipant.addEventListener(self)
if(localParticipant.isLocal){
self.localParticipantViewContainer.isHidden = false
} else {
self.remoteParticipantViewContainer.isHidden = false
}
}
/// Room left
func onRoomLeft(reason: LeaveReason) {
// remove listeners
room?.localParticipant.removeEventListener(self)
room?.removeEventListener(self)
}
/// A new participant joined
func onParticipantJoined(_ participant: Participant) {
participants.append(participant)
// add listener
participant.addEventListener(self)
if(participant.isLocal){
self.localParticipantViewContainer.isHidden = false
} else {
self.remoteParticipantViewContainer.isHidden = false
}
}
/// A participant left from the room
/// - Parameters:
/// - participant: participant object
/// - reason: why the participant left
func onParticipantLeft(_ participant: Participant, reason: LeaveReason) {
participant.removeEventListener(self)
guard let index = self.participants.firstIndex(where: { $0.id == participant.id }) else {
return
}
// remove participant from list
participants.remove(at: index)
// hide from ui
UIView.animate(withDuration: 0.5){
if(!participant.isLocal){
self.remoteParticipantViewContainer.isHidden = true
}
}
}
/// Called when speaker is changed
/// - Parameter activeSpeakerId: participant id of the speaker, nil when no one is speaking.
func onSpeakerChanged(activeSpeakerId: String?) {
// show indication for active speaker
if let participant = participants.first(where: { $0.id == activeSpeakerId }) {
self.showActiveSpeakerIndicator(participant.isLocal ? localParticipantViewContainer : remoteParticipantViewContainer, true)
}
// hide indication for others participants
let otherParticipants = participants.filter { $0.id != activeSpeakerId }
for participant in otherParticipants {
if participants.count > 1 && participant.isLocal {
showActiveSpeakerIndicator(localParticipantViewContainer, false)
} else {
showActiveSpeakerIndicator(remoteParticipantViewContainer, false)
}
}
}
/// Errors the SDK reports on its own, such as a camera or mic that could not be opened
func onError(error: VideoSDKError) {
print("VideoSDK error: \(error.name) (\(error.code)): \(error.message)")
}
func showActiveSpeakerIndicator(_ view: UIView, _ show: Bool) {
view.layer.borderWidth = 4.0
view.layer.borderColor = show ? UIColor.blue.cgColor : UIColor.clear.cgColor
}
}
Step 10: Implementing ParticipantEventListener
In this stage, we'll add an extension for the RoomViewController that implements the ParticipantEventListener, which implements the onStreamEnabled and onStreamDisabled methods for the audio and video of MediaStreams enabled or disabled.
The function updateUI updates the user interface (camera & mic on / off) when an RTCStream is enabled or disabled.
- Swift
class RoomViewController: UIViewController {
//...
}
extension RoomViewController: ParticipantEventListener {
/// Participant has enabled mic, video or screenshare
/// - Parameters:
/// - stream: enabled stream object
/// - participant: participant object
func onStreamEnabled(_ stream: RTCStream, forParticipant participant: Participant) {
updateUI(participant: participant, forStream: stream, enabled: true)
}
/// Participant has disabled mic, video or screenshare
/// - Parameters:
/// - stream: disabled stream object
/// - participant: participant object
func onStreamDisabled(_ stream: RTCStream, forParticipant participant: Participant) {
updateUI(participant: participant, forStream: stream, enabled: false)
}
}
private extension RoomViewController {
func updateUI(participant: Participant, forStream stream: RTCStream, enabled: Bool) { // true
switch stream.kind {
case .video:
if let videotrack = stream.track as? RTCVideoTrack {
if enabled {
DispatchQueue.main.async {
UIView.animate(withDuration: 0.5){
if(participant.isLocal){
self.localParticipantViewContainer.isHidden = false
self.localParticipantVideoView.isHidden = false
self.localParticipantVideoView.videoContentMode = .scaleAspectFill
self.localParticipantViewContainer.bringSubviewToFront(self.localParticipantVideoView)
videotrack.add(self.localParticipantVideoView)
self.lblLocalParticipantNoMedia.isHidden = true
} else {
self.remoteParticipantViewContainer.isHidden = false
self.remoteParticipantVideoView.isHidden = false
self.remoteParticipantVideoView.videoContentMode = .scaleAspectFill
self.remoteParticipantViewContainer.bringSubviewToFront(self.remoteParticipantVideoView)
videotrack.add(self.remoteParticipantVideoView)
self.lblRemoteParticipantNoMedia.isHidden = true
}
}
}
} else {
UIView.animate(withDuration: 0.5){
if(participant.isLocal){
self.localParticipantViewContainer.isHidden = false
self.localParticipantVideoView.isHidden = true
self.lblLocalParticipantNoMedia.isHidden = false
videotrack.remove(self.localParticipantVideoView)
} else {
self.remoteParticipantViewContainer.isHidden = false
self.remoteParticipantVideoView.isHidden = true
self.lblRemoteParticipantNoMedia.isHidden = false
videotrack.remove(self.remoteParticipantVideoView)
}
}
}
}
case .audio:
if participant.isLocal {
localParticipantViewContainer.layer.borderWidth = 4.0
localParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
} else {
remoteParticipantViewContainer.layer.borderWidth = 4.0
remoteParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
}
default:
break
}
}
}
Output

Known Issue
If your video appears outside the container view, as in the image below, add the following lines to the viewDidLoad method in RoomViewController.swift.

- Swift
override func viewDidLoad() {
localParticipantVideoView.frame = CGRect(x: 10, y: 0, width: localParticipantViewContainer.frame.width, height: localParticipantViewContainer.frame.height)
localParticipantVideoView.bounds = CGRect(x: 10, y: 0, width: localParticipantViewContainer.frame.width, height: localParticipantViewContainer.frame.height)
localParticipantVideoView.clipsToBounds = true
remoteParticipantVideoView.frame = CGRect(x: 10, y: 0, width: remoteParticipantViewContainer.frame.width, height: remoteParticipantViewContainer.frame.height)
remoteParticipantVideoView.bounds = CGRect(x: 10, y: 0, width: remoteParticipantViewContainer.frame.width, height: remoteParticipantViewContainer.frame.height)
remoteParticipantVideoView.clipsToBounds = true
}
Stuck anywhere? Check out this example code on GitHub. The example uses iOS SDK 3.x; the Migration Guide lists what changed in 4.0.0.
Got a Question? Ask us on discord

