video sdk logo

Login

iOS Audio Rooms Tutorial

Build drop-in audio rooms in Swift with speaker and listener roles, raise hand, and moderation. AVAudioSession routing and CallKit make the room behave like a native iOS call, in the foreground or the background.
iOS audio room built with VideoSDK in SwiftUI showing speakers on stage and listener controls

Trusted by 300+ global business & tech leaders

Build an Audio-Only Room on iOS, Step by Step

Learn how to build an audio-only room on iOS using the VideoSDK iOS SDK. This tutorial covers meeting setup, speaker and listener modes, raise-hand requests, and mute, moderation, and AVAudioSession routing.


Prerequisites

  • iOS 11.0+
  • Xcode 12.0+
  • Swift 5.0+

App Architecture

This App will contain two screen :

  1. Join Screen : This screen allows user to either create meeting or join predefined meeting.

  2. 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

Xcode template chooser with the App template selected for the VideoSDK iOS audio calling quickstart

Step 3: Add Product Name and Save the project.

Xcode new project sheet showing the Product Name field for the VideoSDK iOS audio calling quickstart

Step 2: VideoSDK Installation

To install VideoSDK, you must initialise the pod on the project by running the following command.

Terminal
1pod init
2

It will create the Podfile in your project folder, open that file and add the dependency for the VideoSDK like below:

Podfile
1pod 'VideoSDKRTC', :git => 'https://github.com/videosdk-live/videosdk-rtc-ios-sdk.git'
2

Podfile in Xcode with the VideoSDKRTC pod dependency added

then run the below code to install the pod:

Terminal
1pod install
2

then declare the permission in Info.plist :

Info.plist
1<key>NSMicrophoneUsageDescription</key>
2<string>Microphone permission description</string>
3

Note: An audio only app publishes the microphone alone, so NSCameraUsageDescription is not required. Leaving it out means iOS never prompts the user for camera access.

Step 3: Project Structure

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
17

Main.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.

MeetingData.swift
1import Foundation
2
3struct MeetingData {
4    let token: String
5    let name: String
6    let meetingId: String
7    let micEnabled: Bool
8}
9
RoomStruct.swift
1import 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}
24

MeetingData 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.

APIService.swift
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}
30

Step 6: Implement Join Screen

Join screen will work as medium to either schedule new meeting or to join existing meeting.

StartMeetingViewController.swift
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}
88

Output

ios_quickstart_join_screen.png

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.

MeetingViewController.swift
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}
77

micEnabled 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.

MeetingViewController.swift
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}
36

Output

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.

MeetingViewController.swift
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}
89

onSpeakerChanged 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.

MeetingViewController.swift
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}
43

Output

Final Output

Build and run the app from Xcode, and allow microphone access when iOS asks for permission.

To test the call:

  1. Create a meeting from the Join Screen on the first device.
  2. Copy the meeting ID shown in lblMeetingId on the Meeting Screen.
  3. Run the app on a second device.
  4. Enter the same meeting ID on the Join Screen.
  5. Join the meeting.
  6. 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.

Summarize with

ChatGPTClaudePerplexityGoogleGrok
Prefer VideoSDK on Google

Table of Contents

Start Building Now With $20 Free Balance

No credit card required to start.

Documentation →Github Repo →

Share This Article

TwitterLinkedInFacebookHacker News

Get started for free today

Grab your API keys and start building with free $20 included in your account. Need enterprise-grade security or custom workloads? Talk to an expert!

Book a demo

Video SDK Logo

United States

28 Geary St, Suite 650,
San Francisco, CA 94108, United States

India flag

India

18th Floor, 1812, The Junomoneta Tower,
Adajan-Hazira Rd, Surat, Gujarat 395009, India

Video SDK Logo
Video SDK Logo
Video SDK Logo
Video SDK Logo
SOLUTIONS

Video KYC

Video Banking

Virtual Claim

Video MER

Telehealth

Astrology

Gaming

Dating

Live Commerce

Auto Proctoring

Interview-as-a-service

Virtual Events

Live Audio Streaming

Ed-Tech

PRODUCTS

AI-Agents

Real-time Audio & Video SDK

Interactive Live Streaming SDK

Real-time Transcription SDK

Character SDK

Open Source Examples

SUCCESS STORIES

Examedi

Coderschool

TYHO

ForagerOne

Immigo

DEVELOPERS

Documentation

Code Samples

Developer Updates

Developer Hub

TOP ARTICLES

What is WebRTC?

Build a React Native Video Calling App

Build a Flutter Video Calling App

RESOURCES

The Protocol by Video SDK

AI Apps

Creator Program

LEGAL

Terms Of Service

Privacy Policy

Cookie Notice

CCPA Notice

Subprocessors

DPA

RSS

COMPANY

Contact Us

Pricing

Support

Blog

Press Kit