Prerequisites
- iOS 11.0+
- Xcode 12.0+
- Swift 5.0+
App Architecture
This App will contain two screen :
Join Screen: This screen allows user to either create meeting or join predefined meeting.Meeting Screen: This screen basically contain local and remote participant view and some meeting controls such as Enable / Disable Mic and Leave meeting.
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
To install VideoSDK, you must initialise the pod on the project by running the following command.
1pod init
2It will create the Podfile in your project folder, open that file and add the dependency for the VideoSDK like below:
1pod 'VideoSDKRTC', :git => 'https://github.com/videosdk-live/videosdk-rtc-ios-sdk.git'
2
then run the below code to install the pod:
1pod install
2then declare the permission in Info.plist :
1<key>NSMicrophoneUsageDescription</key>
2<string>Microphone permission description</string>
3Note: An audio only app publishes the microphone alone, so
NSCameraUsageDescriptionis not required. Leaving it out means iOS never prompts the user for camera access.
Step 3: Project Structure
1iOSQuickStartDemo
2 ├── Models
3 ├── RoomStruct.swift
4 └── MeetingData.swift
5 ├── ViewControllers
6 ├── StartMeetingViewController.swift
7 └── MeetingViewController.swift
8 ├── AppDelegate.swift // Default
9 ├── SceneDelegate.swift // Default
10 └── APIService
11 └── APIService.swift
12 ├── Main.storyboard // Default
13 ├── LaunchScreen.storyboard // Default
14 └── Info.plist // Default
15 Pods
16 └── Podfile
17Main.storyboard Design
Wire the outlets and actions used in Steps 7 and 8 to the views in this storyboard. The code will not run until each @IBOutlet and @IBAction is connected.
Step 4: Create Models
Create swift file for MeetingData and RoomStruct class model for setting data in object pattern.
1import Foundation
2
3struct MeetingData {
4 let token: String
5 let name: String
6 let meetingId: String
7 let micEnabled: Bool
8}
91import Foundation
2
3struct RoomsStruct: Codable {
4 let createdAt, updatedAt, roomID: String?
5 let links: Links?
6 let id: String?
7
8 enum CodingKeys: String, CodingKey {
9 case createdAt, updatedAt
10 case roomID = "roomId"
11 case links, id
12 }
13}
14
15// MARK: - Links
16struct Links: Codable {
17 let getRoom, getSession: String?
18
19 enum CodingKeys: String, CodingKey {
20 case getRoom = "get_room"
21 case getSession = "get_session"
22 }
23}
24MeetingData carries the token, participant name, meeting ID, and the initial mic state from the join screen to the meeting screen. There is no camera flag because this app never publishes video. RoomsStruct decodes the response from the VideoSDK Rooms API.
Step 5: Get Started with APIClient
Before jumping to anything else, we have to write API to generate unique meetingId. You will require Auth token, you can generate it using either using videosdk-server-api-example or generate it from the Video SDK Dashboard for developer.
1import Foundation
2
3let TOKEN_STRING: String = "<AUTH_TOKEN>";
4
5class APIService {
6
7 class func createMeeting(token: String, completion: @escaping (Result<String, Error>) -> Void) {
8 let url = URL(string: "https://api.videosdk.live/v2/rooms")!
9
10 var request = URLRequest(url: url)
11 request.httpMethod = "POST"
12 request.addValue(TOKEN_STRING, forHTTPHeaderField: "authorization")
13
14 URLSession.shared.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
15 DispatchQueue.main.async {
16 if let data = data, let utf8Text = String(data: data, encoding: .utf8)
17 {
18 do{
19 let dataArray = try JSONDecoder().decode(RoomsStruct.self,from: data)
20 completion(.success(dataArray.roomID ?? ""))
21 } catch {
22 print("Error while creating a meeting: \(error)")
23 completion(.failure(error))
24 }
25 }
26 }
27 }).resume()
28 }
29}
30Step 6: Implement Join Screen
Join screen will work as medium to either schedule new meeting or to join existing meeting.
1import Foundation
2import UIKit
3
4class StartMeetingViewController: UIViewController, UITextFieldDelegate {
5
6 private var serverToken = ""
7
8 /// MARK: outlet for create meeting button
9 @IBOutlet weak var btnCreateMeeting: UIButton!
10
11 /// MARK: outlet for join meeting button
12 @IBOutlet weak var btnJoinMeeting: UIButton!
13
14 /// MARK: outlet for meetingId textfield
15 @IBOutlet weak var txtMeetingId: UITextField!
16
17 /// MARK: Initialize the private variable with TOKEN_STRING &
18 /// setting the meeting id in the textfield
19 override func viewDidLoad() {
20 txtMeetingId.delegate = self
21 serverToken = TOKEN_STRING
22 txtMeetingId.text = "PROVIDE-STATIC-MEETING-ID"
23 }
24
25 /// MARK: method for joining meeting through seague named as "StartMeeting"
26 /// after validating the serverToken in not empty
27 func joinMeeting() {
28 txtMeetingId.resignFirstResponder()
29
30 if !serverToken.isEmpty {
31 DispatchQueue.main.async {
32 self.dismiss(animated: true) {
33 self.performSegue(withIdentifier: "StartMeeting", sender: nil)
34 }
35 }
36 } else {
37 print("Please provide auth token to start the meeting.")
38 }
39 }
40
41 /// MARK: outlet for create meeting button tap event
42 @IBAction func btnCreateMeetingTapped(_ sender: Any) {
43 print("show loader while meeting gets connected with server")
44 joinRoom()
45 }
46
47 /// MARK: outlet for join meeting button tap event
48 @IBAction func btnJoinMeetingTapped(_ sender: Any) {
49 if((txtMeetingId.text ?? "").isEmpty){
50
51 print("Please provide meeting id to start the meeting.")
52 txtMeetingId.resignFirstResponder()
53 } else {
54 joinMeeting()
55 }
56 }
57
58 // MARK: - method for creating room api call and getting meetingId for joining meeting
59
60 func joinRoom() {
61
62 APIService.createMeeting(token: self.serverToken) { result in
63 if case .success(let meetingId) = result {
64 DispatchQueue.main.async {
65 self.txtMeetingId.text = meetingId
66 self.joinMeeting()
67 }
68 }
69 }
70 }
71
72 /// MARK: preparing to animate to meetingViewController screen
73 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
74
75 guard let navigation = segue.destination as? UINavigationController,
76 let meetingViewController = navigation.topViewController as? MeetingViewController else {
77 return
78 }
79
80 meetingViewController.meetingData = MeetingData(
81 token: serverToken,
82 name: txtMeetingId.text ?? "Guest",
83 meetingId: txtMeetingId.text ?? "",
84 micEnabled: true
85 )
86 }
87}
88Output

Step 7: Initialize and Join Meeting
Using the provided token and meetingId, we will configure and initialise the meeting in viewDidLoad().
Then, we'll add @IBOutlet for lblLocalParticipantName and lblRemoteParticipantName, which display the local and remote participant in their container views. An audio call has no video track to render, so the containers hold name labels instead of RTCMTLVideoView.
1import UIKit
2import VideoSDKRTC
3import AVFoundation
4
5class MeetingViewController: UIViewController {
6
7 // MARK: - Properties
8
9 // outlet for local participant container view
10 @IBOutlet weak var localParticipantViewContainer: UIView!
11
12 // outlet for label for meeting Id
13 @IBOutlet weak var lblMeetingId: UILabel!
14
15 // outlet for local participant name label
16 @IBOutlet weak var lblLocalParticipantName: UILabel!
17
18 // outlet for remote participant container view
19 @IBOutlet weak var remoteParticipantViewContainer: UIView!
20
21 // outlet for remote participant name label
22 @IBOutlet weak var lblRemoteParticipantName: UILabel!
23
24 /// Meeting data - required to start
25 var meetingData: MeetingData!
26
27 /// current meeting reference
28 private var meeting: Meeting?
29
30 // MARK: - audio participants including self to show in UI
31 private var participants: [Participant] = []
32
33 // MARK: - Lifecycle Events
34
35 override func viewDidLoad() {
36 super.viewDidLoad()
37 // configure the VideoSDK with token
38 VideoSDK.config(token: meetingData.token)
39
40 // init meeting
41 initializeMeeting()
42
43 // set meeting id in button text
44 lblMeetingId.text = "Meeting Id: \(meetingData.meetingId)"
45 }
46
47 override func viewWillAppear(_ animated: Bool) {
48 super.viewWillAppear(animated)
49 navigationController?.navigationBar.isHidden = true
50 }
51
52 override func viewWillDisappear(_ animated: Bool) {
53 super.viewWillDisappear(animated)
54 navigationController?.navigationBar.isHidden = false
55 NotificationCenter.default.removeObserver(self)
56 }
57
58 // MARK: - Meeting
59
60 private func initializeMeeting() {
61
62 // Initialize the VideoSDK
63 meeting = VideoSDK.initMeeting(
64 meetingId: meetingData.meetingId,
65 participantName: meetingData.name,
66 micEnabled: meetingData.micEnabled,
67 webcamEnabled: false // audio only call, so the camera stays off
68 )
69
70 // Adding the listener to meeting
71 meeting?.addEventListener(self)
72
73 // joining the meeting
74 meeting?.join()
75 }
76}
77micEnabled is read from the MeetingData object passed in by the join screen, and webcamEnabled is always false, so the participant publishes the microphone alone as soon as join() succeeds.
Step 8: Implement Controls
After initialising the meeting in previous step. We will now add @IBOutlet for btnLeave and btnToggleMic which can controls the media in meeting.
In the block below, ... marks the code you already wrote in Step 7. Add these members inside the same MeetingViewController class.
1class MeetingViewController: UIViewController {
2
3 ...
4
5 // outlet for leave button
6 @IBOutlet weak var btnLeave: UIButton!
7
8 // outlet for toggle audio button
9 @IBOutlet weak var btnToggleMic: UIButton!
10
11 // bool for mic
12 var micEnabled = true
13
14 // outlet for leave button click event
15 @IBAction func btnLeaveTapped(_ sender: Any) {
16 DispatchQueue.main.async {
17 self.meeting?.leave()
18 self.dismiss(animated: true)
19 }
20 }
21
22 // outlet for toggle mic button click event
23 @IBAction func btnToggleMicTapped(_ sender: Any) {
24 if micEnabled {
25 micEnabled = !micEnabled // false
26 self.meeting?.muteMic()
27 } else {
28 micEnabled = !micEnabled // true
29 self.meeting?.unmuteMic()
30 }
31 }
32
33 ...
34
35}
36Output
Step 9: Implementing MeetingEventListener
In this step, we'll create an extension for the MeetingViewController that implements the MeetingEventListener, which implements the onMeetingJoined, onMeetingLeft, onParticipantJoined, onParticipantLeft, onParticipantChanged, onSpeakerChanged, etc. methods.
Add this extension at file scope in MeetingViewController.swift, below the closing brace of the class.
1extension MeetingViewController: MeetingEventListener {
2
3 /// Meeting started
4 func onMeetingJoined() {
5
6 // handle local participant on start
7 guard let localParticipant = self.meeting?.localParticipant else { return }
8 // add to list
9 participants.append(localParticipant)
10
11 // add event listener
12 localParticipant.addEventListener(self)
13
14 if(localParticipant.isLocal){
15 self.lblLocalParticipantName.text = localParticipant.displayName
16 self.localParticipantViewContainer.isHidden = false
17 } else {
18 self.lblRemoteParticipantName.text = localParticipant.displayName
19 self.remoteParticipantViewContainer.isHidden = false
20 }
21 }
22
23 /// Meeting ended
24 func onMeetingLeft() {
25 // remove listeners
26 meeting?.localParticipant.removeEventListener(self)
27 meeting?.removeEventListener(self)
28 }
29
30 /// A new participant joined
31 func onParticipantJoined(_ participant: Participant) {
32 participants.append(participant)
33
34 // add listener
35 participant.addEventListener(self)
36
37 if(participant.isLocal){
38 self.lblLocalParticipantName.text = participant.displayName
39 self.localParticipantViewContainer.isHidden = false
40 } else {
41 self.lblRemoteParticipantName.text = participant.displayName
42 self.remoteParticipantViewContainer.isHidden = false
43 }
44 }
45
46 /// A participant left from the meeting
47 /// - Parameter participant: participant object
48 func onParticipantLeft(_ participant: Participant) {
49 participant.removeEventListener(self)
50 guard let index = self.participants.firstIndex(where: { $0.id == participant.id }) else {
51 return
52 }
53 // remove participant from list
54 participants.remove(at: index)
55 // hide from ui
56 UIView.animate(withDuration: 0.5){
57 if(!participant.isLocal){
58 self.remoteParticipantViewContainer.isHidden = true
59 }
60 }
61 }
62
63 /// Called when speaker is changed
64 /// - Parameter participantId: participant id of the speaker, nil when no one is speaking.
65 func onSpeakerChanged(participantId: String?) {
66
67 // show indication for active speaker
68 if let participant = participants.first(where: { $0.id == participantId }) {
69 self.showActiveSpeakerIndicator(participant.isLocal ? localParticipantViewContainer : remoteParticipantViewContainer, true)
70 }
71
72 // hide indication for others participants
73 let otherParticipants = participants.filter { $0.id != participantId }
74 for participant in otherParticipants {
75 if participants.count > 1 && participant.isLocal {
76 showActiveSpeakerIndicator(localParticipantViewContainer, false)
77 } else {
78 showActiveSpeakerIndicator(remoteParticipantViewContainer, false)
79 }
80 }
81 }
82
83 func showActiveSpeakerIndicator(_ view: UIView, _ show: Bool) {
84 view.layer.borderWidth = 4.0
85 view.layer.borderColor = show ? UIColor.blue.cgColor : UIColor.clear.cgColor
86 }
87
88}
89onSpeakerChanged matters more in an audio call than in a video one: with no camera feed on screen, the blue border is the only cue showing who is talking.
Step 10: Implementing ParticipantEventListener
In this stage, we'll add an extension for the MeetingViewController that implements the ParticipantEventListener, which implements the onStreamEnabled and onStreamDisabled methods for the audio of MediaStreams enabled or disabled.
The function updateUI is frequently used to control or modify the user interface (enable / disable mic) in accordance with MediaStream state.
1extension MeetingViewController: ParticipantEventListener {
2
3 /// Participant has enabled mic
4 /// - Parameters:
5 /// - stream: enabled stream object
6 /// - participant: participant object
7 func onStreamEnabled(_ stream: MediaStream, forParticipant participant: Participant) {
8 updateUI(participant: participant, forStream: stream, enabled: true)
9 }
10
11 /// Participant has disabled mic
12 /// - Parameters:
13 /// - stream: disabled stream object
14 /// - participant: participant object
15 func onStreamDisabled(_ stream: MediaStream, forParticipant participant: Participant) {
16 updateUI(participant: participant, forStream: stream, enabled: false)
17 }
18}
19
20private extension MeetingViewController {
21
22 func updateUI(participant: Participant, forStream stream: MediaStream, enabled: Bool) { // true
23 switch stream.kind {
24 case .state(value: .audio):
25 DispatchQueue.main.async {
26 if participant.isLocal {
27 self.localParticipantViewContainer.isHidden = false
28 self.lblLocalParticipantName.text = participant.displayName
29 self.localParticipantViewContainer.layer.borderWidth = 4.0
30 self.localParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
31 } else {
32 self.remoteParticipantViewContainer.isHidden = false
33 self.lblRemoteParticipantName.text = participant.displayName
34 self.remoteParticipantViewContainer.layer.borderWidth = 4.0
35 self.remoteParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
36 }
37 }
38 default:
39 break
40 }
41 }
42}
43Output
Final Output
Build and run the app from Xcode, and allow microphone access when iOS asks for permission.
To test the call:
- Create a meeting from the Join Screen on the first device.
- Copy the meeting ID shown in
lblMeetingIdon the Meeting Screen. - Run the app on a second device.
- Enter the same meeting ID on the Join Screen.
- Join the meeting.
- Allow microphone permission on both devices.
Your completed iOS app can now:
- Create a new VideoSDK room
- Join an existing room by meeting ID
- Publish microphone media only, with no camera access requested
- Show the local and remote participant by name
- Show an active speaker indicator
- Show a red border when a participant's microphone is disabled
- Toggle the local microphone
- Leave the meeting and dismiss the meeting screen
Tip: Stuck anywhere? Check out this example code on GitHub. After the basic call works, explore recording, participant events, and other iOS SDK features in the VideoSDK documentation.
