Precall Setup - iOS
A precall screen lets users check their camera, microphone and network before they join a video call.
Why is it necessary?
Without a precall check, users can join a call with a muted microphone, a camera that does not work or a poor connection.
A precall step in your app catches these problems before the user joins.
Using PreCall Functions
Check Permissions
- Begin by ensuring that your application has the necessary permissions to access user devices such as cameras and microphones.
- Utilize the
getAudioPermissionStatus()andgetVideoPermissionStatus()methods of theVideoSDKclass to verify if permissions are granted.
- Swift
let audioPermission = VideoSDK.getAudioPermissionStatus() // audio permission status
let videoPermission = VideoSDK.getVideoPermissionStatus() // video permission status
Request Permissions (if necessary)
-
If permission status is
notDetermined, use thegetAudioPermission()andgetVideoPermission()methods of theVideoSDKclass to prompt users to grant access to their devices. -
Calling the above functions will automatically handle the notDetermined state and prompt the user to grant permission.
-
Both functions return right away, before the user answers. If you need the answer, for example before you create media tracks, call
await AVCaptureDevice.requestAccess(for: .video)orawait AVCaptureDevice.requestAccess(for: .audio)yourself.
- Swift
class StartViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// UI Setup
setupUI()
// prompt user to grant permission when the app starts
VideoSDK.getVideoPermission()
VideoSDK.getAudioPermission()
}
func setupUI() {
// your UI code
}
}
- If the permissions are
deniedorrestrictedafter approving, you need to prompt user to manually approve the permissions from the Settings.AuthorisationDelegatewas removed in 4.0.0, so open Settings from your own method. Below is an example on how you can do it.
- Swift
class StartViewController: UIViewController {
// settings button
@IBOutlet weak var openSetting: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// UI Setup
setupUI()
// prompt user to grant permission when the app starts
VideoSDK.getVideoPermission()
VideoSDK.getAudioPermission()
}
func setupUI() {
// your UI code
settingsButton()
}
//sets the visibility of settings button
func settingsButton() {
let audioStatus = VideoSDK.getAudioPermissionStatus()
let videoStatus = VideoSDK.getVideoPermissionStatus()
if (audioStatus == .authorized && videoStatus == .authorized) {
openSetting.isHidden = true
}
else {
openSetting.isHidden = false
}
}
// settings button tapped
@IBAction func openSettingsButtonTapped(_ sender: Any) {
handlePermission()
}
}
extension StartViewController {
// opens the app's page in the Settings app
func handlePermission() {
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return }
if UIApplication.shared.canOpenURL(settingsURL) {
UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil)
}
}
}
Get Device Lists
- Once you have the necessary permissions, fetch the lists of available cameras and audio devices using the
getCameras()andgetMics()methods of theVideoSDKclass respectively. - Each entry has a
labelto show in a picker and adeviceIdto pass back to the SDK.getMics()lists theSpeakeroutput first, then each microphone.
- Swift
let cameras = VideoSDK.getCameras() // [CameraDeviceInfo]
let audioDevices = VideoSDK.getMics() // [MicrophoneDeviceInfo]
Create Media Tracks
- Create media tracks for the selected microphone and camera using the
createMicrophoneAudioTrack()andcreateCameraVideoTrack()methods. - Ensure that these tracks originate from the user-selected devices for accurate testing. Pass the
deviceIdof the camera and microphone the user picked fromgetCameras()andgetMics()ascameraIdandmicrophoneId. - These methods never ask for permission. Until access is granted, they throw
ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSEDorERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED. - Both methods are
async throws, so call them withtry awaitfrom aTaskor anasyncfunction. They wait up to 2 seconds for the device to open. If the device fails after that, the track ends and itsonEndedhandler receives the error.
- Swift
var videoMediaTrack: CustomStreamTrack?
var audioMediaTrack: CustomStreamTrack?
do {
// Create custom Video Track
videoMediaTrack = try await VideoSDK.createCameraVideoTrack(
// This will accept the enum value of CustomCameraTrackConfig which contains resolution (height x width) of video you want to capture.
encoderConfig: .h720p_w1280p, // .h540p_w960p | .h720p_w1280p ... // Default : .h720p_w1280p
// It will specify whether to use front or back camera for the video track.
facingMode: .front, // .back, Default : .front
// It will specify whether to send multiple resolution layers or a single layer.
multiStream: true, // false, Default : true
// The deviceId of the camera the user selected from getCameras(). When set, it is used instead of facingMode.
cameraId: selectedCamera.deviceId, // Default : nil
// The video codec. H264, VP9 and AV1 need multiStream: false.
codec: .VP8 // .H264 | .VP9 | .AV1, Default: .VP8
)
// Create custom Audio Track
audioMediaTrack = try await VideoSDK.createMicrophoneAudioTrack(
// The deviceId of the microphone the user selected from getMics().
microphoneId: selectedMicrophone.deviceId, // Default : nil
noiseConfig: NoiseConfig(
noiseSuppression: true,
echoCancellation: true,
autoGainControl: true,
highPassFilter: true
)
)
} catch {
// For example ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED when camera access is not granted
print("Failed to create media tracks: \(error)")
}
Network Quality Assessment
- Run
runPreCallTest()as a pre-flight check before the user joins the room. - It checks that the camera and microphone work. Then it measures how well the network carries a call, for uplink and downlink, audio and video. It returns a quality score and the stats behind it.
Parameters
VideoSDK.runPreCallTest(_:onStatsChange:) takes a PreCallTestOptions value and an optional stats callback.
-
token:- The authentication token used to authorize the test.
- It has to be of
Stringtype. - This is a REQUIRED parameter.
-
samplingDuration:- Controls how long (in milliseconds) the network-stats phase runs.
- It has to be of
Inttype, between10000and120000. - This is an
OPTIONALparameter. - Default:
15000ms
-
audioOnly:- Skips the camera test and runs the pre-call test using audio only; the result's
camerafield is thennil. - It has to be of
Booltype. - This is an
OPTIONALparameter. - Default:
false
- Skips the camera test and runs the pre-call test using audio only; the result's
-
videoTrack:- A camera
CustomStreamTrackyou have already created withcreateCameraVideoTrack(), such as the one rendered on your precall screen. - The SDK never stops a track you pass in. It stays yours to manage after the test.
- This is an
OPTIONALparameter.
- A camera
-
audioTrack:- A microphone
CustomStreamTrackyou have already created withcreateMicrophoneAudioTrack(). - The SDK never stops a track you pass in. It stays yours to manage after the test.
- This is an
OPTIONALparameter.
- A microphone
-
videoConfig:- A
PreCallVideoConfigused to acquire the camera track whenvideoTrackis not provided. - Accepts
cameraId,encoderConfig,facingMode,multiStream,bitrateModeandmaxLayer. - The track created from this configuration is returned live in the result, so you can hand it straight to the room.
- This is an
OPTIONALparameter.
- A
-
audioConfig:- A
PreCallAudioConfigused to acquire the microphone track whenaudioTrackis not provided. - Accepts
microphoneId,encoderConfigandnoiseConfig. - The track created from this configuration is returned live in the result, so you can hand it straight to the room.
- This is an
OPTIONALparameter.
- A
-
onStatsChange:- A closure called on the main actor each time the stats update while sampling runs. It receives the latest stats as a
PreCallNetworkQuality, the same type as the result'snetworkQuality. - This is an
OPTIONALparameter.
- A closure called on the main actor each time the stats update while sampling runs. It receives the latest stats as a
Each of these rules rejects the test with ERROR_PRECALL_INVALID_CONFIG, and the specific cause is preserved in the error's message:
samplingDurationhas to be inside the 10000–120000 ms range.- Pass
videoTrackorvideoConfig, not both. PassaudioTrackoraudioConfig, not both. - Do not combine
audioOnly: truewithvideoTrackorvideoConfig.
- Only one test can run at a time, and a test cannot run while a room is initialized. Run it before
createRoom(): after that, the test is refused withERROR_PRECALL_AFTER_INITuntil the room is left. - Camera and microphone permission must already be granted, because the test does not ask for it.
- If your precall screen already shows tracks, pass them as
videoTrackandaudioTrack. The SDK has one camera capture and one microphone capture, so when the test opens a device itself, the track you created earlier for that device ends. - Cancelling the calling
Taskstops the test, likestopPreCallTest(). A stopped test still returns a result, withabortedset totrue.
Example
import VideoSDKRTC
Task {
do {
let result = try await VideoSDK.runPreCallTest(
// Test the tracks your precall screen already shows
PreCallTestOptions(
token: token,
samplingDuration: 15_000,
videoTrack: videoMediaTrack,
audioTrack: audioMediaTrack
)
) { stats in
// Called on the main actor each time the stats update while sampling
print("uplink quality:", stats.uplink.quality)
}
print("camera ok:", result.camera?.status ?? false)
print("microphone ok:", result.microphone?.status ?? false)
print("downlink quality:", result.networkQuality?.downlink.quality ?? 0)
} catch let error as PreCallTestError {
print(error.code, error.message)
} catch {
print("Failed to run pre-call test: \(error)")
}
}
To stop a running test early:
Task {
await VideoSDK.stopPreCallTest()
}
Result
runPreCallTest() returns a PreCallTestResult:
| Property | Type | Description |
|---|---|---|
aborted | Bool | true when the test was stopped before it finished. |
testDuration | Int | How long the test actually ran, in milliseconds. |
camera | PreCallMediaReport? | The camera check. nil when audioOnly is true. |
microphone | PreCallMediaReport? | The microphone check. |
networkQuality | PreCallNetworkQuality? | The uplink and downlink measurements. |
PreCallMediaReport carries status, track, captureResolution, fps, codec and error. When the SDK opened the device for you (you did not pass videoTrack or audioTrack), track is the live track. Use it for the room instead of creating another one. It stays open after the test, so call its stop() if you do not use it.
PreCallNetworkQuality carries audioOnly plus uplink and downlink, each a PreCallDirectionQuality:
| Property | Type | Description |
|---|---|---|
quality | Int | The score for that direction. |
factors | [String] | What limited the score. |
audio | PreCallAudioQuality? | quality, rtt, bitrate, packetLoss, jitter, bytesSent, bytesReceived. |
video | PreCallVideoQuality? | The audio fields plus fps, resolution, qualityLimitationReason, framesDropped, freezeCount, framesDroppedRatio and totalFreezesDuration. |
Errors
Failures throw PreCallTestError, which carries code, name and message. Note that code is a String here, not an integer.
| Code | When |
|---|---|
ERROR_PRECALL_INVALID_TOKEN | The token is empty. |
ERROR_PRECALL_INVALID_CONFIG | One of the option rules above was broken. |
ERROR_PRECALL_AFTER_INIT | A room was already initialized. |
ERROR_PRECALL_TEST_ALREADY_RUNNING | Another test is still running. |
ERROR_PRECALL_MEDIA_CHECK_FAILED | Neither the camera nor the microphone could be opened. |
ERROR_PRECALL_TEST_FAILED | The network test could not be completed, for example because the server refused the token. |
If only one device fails its check, the test does not throw. That device's report has status set to false and carries the error. The test throws the device's own error, such as ERROR_INVALID_CUSTOM_VIDEO_TRACK or ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED, when a track you passed cannot be used or the microphone fails in an audio-only test.
Passing States to Room
- Ensure that all relevant states, such as microphone and camera status (on/off) are passed into the room from the precall screen.
- Pass these states and media tracks to
createRoom. - Users then keep their precall settings when they join the room.
- Swift
do {
room = try VideoSDK.createRoom(
roomId: "abcd-efgh-xyzw",
token: token,
participantName: "John Doe", // optional
micEnabled: micStatus,
camEnabled: camStatus,
participantId: "JD", // optional
mode: .SEND_AND_RECV, //optional (default mode is SEND_AND_RECV)
customCameraTrack: videoMediaTrack, //optional
customMicrophoneTrack: audioMediaTrack //optional
)
} catch {
print("Failed to create the room: \(error)")
}
API Reference
The API references for all the methods utilized in this guide are provided below.
Got a Question? Ask us on discord

