Dolby.io provides Communication APIs that allows developers to create their own types of conference calling applications in a self-serve way. Here at Dolby.io, we provide an iOS SDK that makes it as simple as possible to set up in your own application. In this blog post, you’ll learn how to create an video conference call using Dolby.io and Xcode.
Pre-requisites
- Latest version of Xcode
- Apple Developer Account
- Physical Device
- A Dolby.io Account
What the App will look like

Building the app
We’ll build this app using Swift and UIKit.
Here’s what the final app looks like. View the GitHub repo here!
Setup
Dolby.io Setup – Creating a Dolby.io Account
Before coding, we’re going to have to create a Dolby.io account. Go to Dolby.io and click on sign up to get started. Once you’re registered, click on Add New App in the dashboard page. For this blog post, we’ll name the app iOS Audio Video Call but feel free to name it whatever you’d like!
Once we’ve got that created, you’ll see a new applications under the Applications tab on the dashboard. Now whenever we need the Communications API keys, we can access them by either clicking on our application under the Applications tab or on the left side-bar.
Setting Up Your Project in Xcode
Create Project with Basic Storyboard Template
- Open Xcode and create a new iOS app. Select Storyboard for the User Interface and Swift for the Language option.
Update info.plist
- Camera Permissions and App Capabilities
- Set the application Permission in the info.pList.
- Establish privacy permissions by adding two new keys with the suggested values in Info.plist:
- Select Privacy – Microphone Usage Description.
- Add this value: “This app requires the use of the microphone to join a conference”
- Select Privacy – Camera Usage Description
- Value: “This app requires the use of the camera to join a conference”
Set the application capabilities screen
- Go to your target settings to enable the background mode and select: Signing & Capabilities ▸ + Capability ▸ Background Modes.
- Turn on Audio, AirPlay, and Picture in Picture and Voice over IP.
- Set Your Team and Credential
- Select your team and set the project’s bundle ID.; Typically, com.teamid.ios.quickstart where teamid equals your team id.

Test App on your Device
Deploy the App to your phone to make sure your device is ready for development.
You should see the basic app running.
If you see a pop-up warning you to trust the app first before opening, head over to your settings app > search for “Profile” > click on VPN & Device Management > Click on the Developer App > Click on Trust > run the app!



Note: Do not proceed If you run into problems running the basic app on your device; consult the Apple Developer documentation on getting started with the iOS Xcode toolchain.
SDK Setup
- Add the Dolby.io SDK to your project using Swift Package Manager (SPM):
- Select File ▸ Add Packages… to add package dependency.
- In the opened window, find the search box and specify the URL to the SDK repository: https://github.com/voxeet/voxeet-sdk-ios
- Choose voxeet-sdk-ios from the results list.
- Select the proper SDK version from the Dependency Rule dropdown list.

- Select the Add Package option.
- Set the byte code in the project build settings
Initializing the VoxeetSDK
Go to your Dolby.io dashboard and grab your Demos client access token from the application we created earlier.
We’ll be using this generated demos client access token to securely initialize our app.

Create a Constants file to hold and set an API Token constant, Explain developer token
- Create new file called
Constants.swift
. - Add the line:
// Constants.swift
let API_TOKEN = "<REPLACE-WITH-YOUR-DEVELOPER-TOKEN>"
Implementing our Swift Files
AppDelegate
- Initialize VoxeetSDK in the first application function of the
AppDelegate.swift
file - Set notification.type to .none and defaultBuiltInSpeaker and defaultVideo to false.
- Set notification.type to .none and defaultBuiltInSpeaker and defaultVideo to false.
- Determine UI and App Controls in ViewController
- Define what visual components will be needed within ViewController’s main class.
Don’t worry if you see errors with the #selector(action)
or delegates
. We will be implementing this in the next step!
import UIKit
import VoxeetSDK
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Voxeet SDK OAuth initialization.
// API_TOKEN is coming from our ``Constants.swift`` file we created earlier
VoxeetSDK.shared.initialize(accessToken: API_TOKEN) { (refreshClosure, isExpired) in
refreshClosure(token)
}
VoxeetSDK.shared.notification.push.type = .none
VoxeetSDK.shared.conference.defaultBuiltInSpeaker = true // start conference with audio on
VoxeetSDK.shared.conference.defaultVideo = false // start conference with video off
return true
}
Then we’re going to move onto setting up the UI inside the ViewController.swift
file.
In this first part, we’ll set up the views that make the UI for the app.
import UIKit
import VoxeetSDK
class ViewController: UIViewController {
// Session UI.
var sessionTextField: UITextField!
var logInButton: UIButton!
var logoutButton: UIButton!
// Conference UI.
var conferenceTextField: UITextField!
var startButton: UIButton!
var leaveButton: UIButton!
var startVideoButton: UIButton!
var stopVideoButton: UIButton!
// Videos views.
var videosView1: VTVideoView!
var videosView2: VTVideoView!
// Participant label.
var participantsLabel: UILabel!
// User interface settings.
let margin: CGFloat = 16
let buttonWidth: CGFloat = 120
let buttonHeight: CGFloat = 35
let textFieldWidth: CGFloat = 120 + 16 + 120
let textFieldHeight: CGFloat = 40
...
}
Stub out the application logic into the viewDidLoad()
function
- Create empty initSessionUI and InitConference functions to hold the logic to create the UI Controls and conference functions.
- Add each to the ViewController’s viewDidLoad method.
import UIKit
import VoxeetSDK
class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
// Init UI.
initSessionUI()
initConferenceUI()
// Conference delegate.
VoxeetSDK.shared.conference.delegate = self
}
}
Implementing the initSessionUI()
function
Update initSessionUI method with the following code; to learn more about code, take a look at the comments for details on what’s happening.
func initSessionUI() {
/*
First we return the statusBarHeight - since this value changes depending on the physical device and orientation we use this handy routine to determine the height based on window and scene via the statusBarManager.
*/
var statusBarHeight: CGFloat {
// 13.0 and later
if #available(iOS 13.0, *){
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
return window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0;
}else {
return UIApplication.shared.statusBarFrame.height
}
}
/*
Next generate an array of random names - we'll use names of our favorite Avengers, feel free to use any names here.
*/
let randomNames = ["Thor",
"Cap",
"Tony Stark",
"Black Panther",
"Black Widow",
"Hulk",
"Spider-Man"]
/**
Next we will create an instance of each ui control by setting its frame and attributes; and finally adding it to our view controller's view as a subview. We add the sessionTextField, logInButton, logoutButton to the view as a subview; these controls will be used later to set the conference username and essentially start and end the session.
**/
// Session text field.
sessionTextField = UITextField(frame: CGRect(x: margin,
y: statusBarHeight + margin,
width: textFieldWidth, height: textFieldHeight))
sessionTextField.borderStyle = .roundedRect
sessionTextField.placeholder = "Username"
sessionTextField.autocorrectionType = .no
sessionTextField.text = randomNames.randomElement()
sessionTextField.delegate = self
self.view.addSubview(sessionTextField)
// Open session button.
logInButton = UIButton(type: .system) as UIButton
logInButton.frame = CGRect(x: margin,
y: sessionTextField.frame.origin.y + sessionTextField.frame.height + margin,
width: buttonWidth, height: buttonHeight)
logInButton.backgroundColor = logInButton.tintColor
logInButton.layer.cornerRadius = 5
logInButton.isEnabled = true
logInButton.isSelected = true
logInButton.setTitle("LOG IN", for: .normal)
logInButton.addTarget(self, action: #selector(logInButtonAction), for: .touchUpInside)
self.view.addSubview(logInButton)
// Close session button.
logoutButton = UIButton(type: .system) as UIButton
logoutButton.frame = CGRect(x: logInButton.frame.origin.x + logInButton.frame.width + margin,
y: logInButton.frame.origin.y,
width: buttonWidth, height: buttonHeight)
logoutButton.backgroundColor = logoutButton.tintColor
logoutButton.layer.cornerRadius = 5
logoutButton.isEnabled = false
logoutButton.isSelected = true
logoutButton.setTitle("LOGOUT", for: .normal)
logoutButton.addTarget(self, action: #selector(logoutButtonAction), for: .touchUpInside)
self.view.addSubview(logoutButton)
}
Implementing the initConferenceUI() function
Update initConferenceUI method with the following code:
func initConferenceUI() {
// Session text field.
conferenceTextField = UITextField(frame: CGRect(x: margin,
y: logoutButton.frame.origin.y + logoutButton.frame.height + margin,
width: textFieldWidth, height: textFieldHeight))
conferenceTextField.borderStyle = .roundedRect
conferenceTextField.placeholder = "Conference alias"
conferenceTextField.autocorrectionType = .no
conferenceTextField.text = "dev-portal"
conferenceTextField.delegate = self
self.view.addSubview(conferenceTextField)
// Conference create/join button.
startButton = UIButton(type: .system) as UIButton
startButton.frame = CGRect(x: margin,
y: conferenceTextField.frame.origin.y + conferenceTextField.frame.height + margin,
width: buttonWidth, height: buttonHeight)
startButton.backgroundColor = startButton.tintColor
startButton.layer.cornerRadius = 5
startButton.isEnabled = false
startButton.isSelected = true
startButton.setTitle("START", for: .normal)
startButton.addTarget(self, action: #selector(startButtonAction), for: .touchUpInside)
self.view.addSubview(startButton)
// Conference leave button.
leaveButton = UIButton(type: .system) as UIButton
leaveButton.frame = CGRect(x: startButton.frame.origin.x + startButton.frame.width + margin,
y: startButton.frame.origin.y,
width: buttonWidth, height: buttonHeight)
leaveButton.backgroundColor = leaveButton.tintColor
leaveButton.layer.cornerRadius = 5
leaveButton.isEnabled = false
leaveButton.isSelected = true
leaveButton.setTitle("LEAVE", for: .normal)
leaveButton.addTarget(self, action: #selector(leaveButtonAction), for: .touchUpInside)
self.view.addSubview(leaveButton)
// Start video button.
startVideoButton = UIButton(type: .system) as UIButton
startVideoButton.frame = CGRect(x: margin,
y: startButton.frame.origin.y + startButton.frame.height + margin,
width: buttonWidth, height: buttonHeight)
startVideoButton.backgroundColor = startVideoButton.tintColor
startVideoButton.layer.cornerRadius = 5
startVideoButton.isEnabled = false
startVideoButton.isSelected = true
startVideoButton.setTitle("START VIDEO", for: .normal)
startVideoButton.addTarget(self, action: #selector(startVideoButtonAction), for: .touchUpInside)
self.view.addSubview(startVideoButton)
// Stop video button.
stopVideoButton = UIButton(type: .system) as UIButton
stopVideoButton.frame = CGRect(x: startButton.frame.origin.x + startButton.frame.width + margin,
y: startVideoButton.frame.origin.y,
width: buttonWidth, height: buttonHeight)
stopVideoButton.backgroundColor = stopVideoButton.tintColor
stopVideoButton.layer.cornerRadius = 5
stopVideoButton.isEnabled = false
stopVideoButton.isSelected = true
stopVideoButton.setTitle("STOP VIDEO", for: .normal)
stopVideoButton.addTarget(self, action: #selector(stopVideoButtonAction), for: .touchUpInside)
self.view.addSubview(stopVideoButton)
/*
To emulate a popular video chat app layout;
We'll create a large VTVideoView to display the 2nd participant and a smaller VTVideoView for the current participant.
*/
// Video views.
videosView1 = VTVideoView(frame: CGRect(x: margin,
y: startVideoButton.frame.origin.y + startVideoButton.frame.height + margin,
width: buttonWidth, height: buttonWidth))
videosView1.backgroundColor = .black
videosView2 = VTVideoView(frame: CGRect(x:margin,
y: startVideoButton.frame.origin.y + startVideoButton.frame.height + margin,
width: self.view.frame.width - (margin * 2), height: self.view.frame.height / 2))
videosView2.backgroundColor = .black
/*
Note the order of adding the VTVideoView to the main view. Simply by swapping the oder in which we add a view to the main view determines the z-order of the elements.
We'll overlay the current participant on top of the 2nd participant's VTVideoView.
*/
self.view.addSubview(videosView2)
self.view.addSubview(videosView1)
/*
Finally we add a label to indicate the name of the participants who have joined the conference.
*/
// Participants label.
participantsLabel = UILabel(frame: CGRect(x: margin,
y: videosView2.frame.origin.y + videosView2.frame.height + margin,
width: textFieldWidth, height: textFieldHeight))
participantsLabel.backgroundColor = .lightGray
participantsLabel.adjustsFontSizeToFitWidth = true
participantsLabel.minimumScaleFactor = 0.1
self.view.addSubview(participantsLabel)
}
Handling Button Actions
Just below our two methods add the button actions that call the SDK API to manage various operations.
/**
First we create and open a session by calling VoxeetSDK.shared.session.open(info: info) with an VTParticipantInfo object that represents our current participant; here we set the name, the externalID: avatarURL are set to nil, you can optionally set an external ID and URL to an image to represent the current participant. The method returns a closure where we can setup UI state and capture any errors
**/
@objc func logInButtonAction(sender: UIButton!) {
// Open user session.
let info = VTParticipantInfo(externalID: nil, name: sessionTextField.text, avatarURL: nil)
VoxeetSDK.shared.session.open(info: info) { error in
self.logInButton.isEnabled = false
self.logoutButton.isEnabled = true
self.startButton.isEnabled = true
self.leaveButton.isEnabled = false
}
}
@objc func logoutButtonAction(sender: UIButton!) {
// Close user session
// To close the session we call VoxeetSDK.shared.session.close
VoxeetSDK.shared.session.close { error in
self.logInButton.isEnabled = true
self.logoutButton.isEnabled = false
self.startButton.isEnabled = false
self.leaveButton.isEnabled = false
}
}
@objc func startButtonAction(sender: UIButton!) {
/**
We create a conference with options, notably we can explicitly set Dolby Voice and the Live Recording params. Note: by default, Dolby Voice is enabled.
As with the previous methods, we set the UI state and manage errors in the closure that is returned by the method.
**/
// Create a conference room with an alias.
let options = VTConferenceOptions()
options.alias = conferenceTextField.text ?? ""
options.params.dolbyVoice = true
options.params.liveRecording = true; // Enables generation of the recording at the end of the conference
VoxeetSDK.shared.conference.create(options: options, success: { conference in
// Join the conference with its id.
VoxeetSDK.shared.conference.join(conference: conference, success: { response in
self.logoutButton.isEnabled = false
self.startButton.isEnabled = false
self.leaveButton.isEnabled = true
self.startVideoButton.isEnabled = true
}, fail: { error in })
}, fail: { error in })
}
@objc func leaveButtonAction(sender: UIButton!) {
//Leave a conference and manage UI state and errors
VoxeetSDK.shared.conference.leave { error in
self.logoutButton.isEnabled = true
self.startButton.isEnabled = true
self.leaveButton.isEnabled = false
self.startVideoButton.isEnabled = false
self.stopVideoButton.isEnabled = false
self.participantsLabel.text = nil
}
}
@objc func startVideoButtonAction(sender: UIButton!) {
/**
Start Video and update the UI State.
Start Video is an asynchronous methods that trigger the streamAdded delegate method update event/
We'll cover the SDK delegate methods in more detail below to understand how the video is attached to the VTVideoView. The basic takeaway is understanding that calling start or stop triggers that stream updated event.
**/
VoxeetSDK.shared.conference.startVideo { error in
if error == nil {
self.startVideoButton.isEnabled = false
self.stopVideoButton.isEnabled = true
}
}
}
@objc func stopVideoButtonAction(sender: UIButton!) {
// Stop Video and update the UI state
VoxeetSDK.shared.conference.stopVideo { error in
if error == nil {
self.startVideoButton.isEnabled = true
self.stopVideoButton.isEnabled = false
}
}
}
}
Add Conference Delegate Methods
We’ll create abs extension to our ViewController that supports the VTConferenceDelegate protocol.
Just below our main view controller class create your extension:
Xcode will provide a warning that the extension fails to conform to the protocol – allow Xcode to fix the errors and stub in all of the required methods. Next update the extension to include the additional code; review the comments to understand the flow logic.
extension ViewController: VTConferenceDelegate {
func permissionsUpdated(permissions: [Int]) {}
func participantAdded(participant: VTParticipant) {}
func participantUpdated(participant: VTParticipant) {}
func statusUpdated(status: VTConferenceStatus) {}
func streamAdded(participant: VTParticipant, stream: MediaStream) {
streamUpdated(participant: participant, stream: stream)
}
/*
Called when the session a new session was initially created with a participant; We simply pass the stream and participant to our streamUpdated function.
*/
func streamUpdated(participant: VTParticipant, stream: MediaStream) {
if participant.id == VoxeetSDK.shared.session.participant?.id {
if !stream.videoTracks.isEmpty {
videosView1.attach(participant: participant, stream: stream)
} else {
videosView1.unattach() /* Optional */
}
} else {
/**
Otherwise the participant did not match, therefore if there's a video track it's the next (2nd) participant.
This time we attach the stream and participant to videView2.
**/
if !stream.videoTracks.isEmpty {
videosView2.attach(participant: participant, stream: stream)
} else {
videosView2.unattach() /* Optional */
}
}
// Update participants label.
updateParticipantsLabel()
}
func streamRemoved(participant: VTParticipant, stream: MediaStream) {
updateParticipantsLabel()
}
/**
Finally we update the UI's participant label.
**/
func updateParticipantsLabel() {
// Update participants label.
/**
We return a list of participants and filter the streams and map the names from the participant info object into an array of names. The we create a list of the names using join and display them in the UI by setting the text of the particpantsLabel
**/
let participants = VoxeetSDK.shared.conference.current?.participants
.filter({ $0.streams.isEmpty == false })
let usernames = participants?.map({ $0.info.name ?? "" })
participantsLabel.text = usernames?.joined(separator: ", ")
}
}
Add UITextField Delegate Method
We need to add one more extension to the ViewController to handle the dismissal of the keyboard when a user taps into the Textfield. Add this extension to the end of the ViewController.swift
file.
Extend this delegate to dismiss keyboard.
extension ViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.view.endEditing(true)
return false
}
}
Compile and Run the App

Congratulations! You’ve successfully created your first basic video conference app using Dolby.io and iOS.