Precall Setup - Flutter
Picture this: before diving into the depths of a video call, imagine giving your setup a quick check-up, like a tech-savvy doctor ensuring all systems are a go. That's essentially what a precall experience does- it’s like your extensive debug session before the main code execution—a crucial step in ensuring your app's performance is top-notch.
Step-by-Step Guide: Integrating Precall Feature
Step 1: Check Permissions
- Begin by ensuring that your application has the necessary permissions to access user devices such as cameras and microphones.
- Utilize the
checkPermissions()method of theVideoSDKclass to verify if permissions are granted. It only checks, and never asks the user. - On iOS, permission checks and requests need the camera and microphone usage descriptions in your app's
Info.plist, as shown in Integrate Flutter SDK. If you add these keys after your first iOS build, delete Xcode'sDerivedDatafolder once, so that the next build picks them up.
import 'package:videosdk/videosdk.dart';
Future<void> checkMediaPermissions() async {
try {
//By default both audio and video permissions will be checked.
Map<String, bool> checkPermissions = await VideoSDK.checkPermissions();
//For checking just audio permission.
Map<String, bool> checkAudioPermissions = await VideoSDK.checkPermissions(permissions: Permissions.AUDIO);
//For checking just video permission.
Map<String, bool> checkVideoPermissions = await VideoSDK.checkPermissions(permissions: Permissions.VIDEO);
//For checking both audio and video permissions.
Map<String, bool> checkAudioVideoPermissions = await VideoSDK.checkPermissions(permissions: Permissions.AUDIO_VIDEO);
} catch (ex) {
print("Error in checkPermissions()");
}
// Output: Map object for both audio and video permission:
// {audio: true, video: true}
}
- When microphone and camera permissions are blocked, the user cannot preview or test those devices:

Step 2: Request Permissions (if necessary)
- If permissions are not granted, use the
requestPermissions()method of theVideoSDKclass to prompt users to grant access to their devices. - On Android, the SDK does not ask for camera or microphone permission when it creates a track, when you join, or when you turn a device on. Request the permissions here, before you create tracks or join the room.
- On iOS, requesting permissions needs the same
Info.plistentries as Step 1.
In case permissions are blocked by the user, the permission request dialogue cannot be re-rendered programmatically. In such cases, consider providing guidance to users on manually adjusting their permissions.
Future<void> requestMediaPermissions() async {
try {
//By default both audio and video permissions will be requested.
Map<String, bool> reqPermissions = await VideoSDK.requestPermissions();
//For requesting just audio permission.
Map<String, bool> reqAudioPermissions = await VideoSDK.requestPermissions(Permissions.AUDIO);
//For requesting just video permission.
Map<String, bool> reqVideoPermissions = await VideoSDK.requestPermissions(Permissions.VIDEO);
//For requesting both audio and video permissions.
Map<String, bool> reqAudioVideoPermissions = await VideoSDK.requestPermissions(Permissions.AUDIO_VIDEO);
} catch (ex) {
print("Error in requestPermission ");
}
}
- Requesting permissions if not already granted:

Step 3: Render Device List
- Once you have the necessary permissions, Fetch and render list of available video and audio devices using the
getCameras()andgetMics()methods of theVideoSDKclass respectively. - Enable users to select their preferred devices from these lists.
- On Android and iOS,
getMics()returns audio routes, such as the speaker, the earpiece, a wired headset or a Bluetooth device. Selecting one changes both the microphone and the speaker. - On Android, the audio devices are
SPEAKER_PHONE,EARPIECE,WIRED_HEADSETandBLUETOOTH.
import 'package:videosdk/videosdk.dart';
List<CameraDeviceInfo> videoDevices = [];
List<MicrophoneDeviceInfo> audioDevices = [];
Future<void> getMediaDevices() async {
try {
//Method to get all available video devices.
videoDevices = await VideoSDK.getCameras() ?? [];
//Method to get all available audio devices.
audioDevices = await VideoSDK.getMics() ?? [];
} catch (err) {
print("Error in getting audio or video devices");
}
}
- Displaying device lists once permissions are granted:

Step 4: Handle Device Changes
- Before a room exists, the SDK does not report device changes. To show devices that were attached or removed, fetch the lists again with
getCameras()andgetMics(), for example each time the user opens a device picker. - Once you have created the room,
room.setAudioDeviceChangeListener()tells you when a headset or Bluetooth device connects or disconnects. See Change Audio Output Device.
Step 5: Ensuring the selected devices are used in the Room
- Ensure that all relevant states, such as microphone and camera status (on/off), and selected devices, are passed into the room from the precall screen.
- This can be accomplished by passing these crucial states and media streams onto the VideoSDK and Room Methods.
- For the video device, create a custom track with the
deviceIdof the selected camera and pass it to thecreateRoommethod. If your precall screen already shows a preview track, pass that track. If the user leaves without joining, callstop()on the track to close the camera. - For the audio device, call the
changeMicmethod with the selected device before you calljoin(). The room uses that device when it joins. - Users then join the room with the settings they picked in the precall setup.
import 'dart:async' show unawaited;
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
class RoomScreen extends StatefulWidget {
final String roomId;
final String token;
// Created on the precall screen with
// VideoSDK.createCameraVideoTrack(cameraId: selectedVideoDevice?.deviceId)
final CustomStreamTrack? cameraTrack;
// Selected on the precall screen from VideoSDK.getMics()
final MicrophoneDeviceInfo? selectedAudioDevice;
const RoomScreen({
super.key,
required this.roomId,
required this.token,
this.cameraTrack,
this.selectedAudioDevice,
});
@override
State<RoomScreen> createState() => _RoomScreenState();
}
class _RoomScreenState extends State<RoomScreen> {
late Room _room;
@override
void initState() {
super.initState();
// create room
_room = VideoSDK.createRoom(
roomId: widget.roomId,
token: widget.token,
participantName: "John Doe",
micEnabled: true,
camEnabled: true,
customCameraTrack: widget.cameraTrack, //For Video Devices
);
unawaited(joinRoom());
}
// Camera and microphone permissions were requested on the precall screen (Step 2).
Future<void> joinRoom() async {
//For Audio Devices: a device selected before join() is used when the room joins.
final MicrophoneDeviceInfo? audioDevice = widget.selectedAudioDevice;
if (audioDevice != null) {
try {
await _room.changeMic(deviceId: audioDevice.deviceId);
} on VideoSDKError catch (error) {
// The device is gone, for example a Bluetooth headset was disconnected.
print("Could not select ${audioDevice.label}: ${error.code} ${error.name}");
}
}
try {
await _room.join();
} on VideoSDKError catch (error) {
print("Join failed: ${error.code} ${error.name}");
}
}
@override
Widget build(BuildContext context) {
return YourRoomWidget();
}
}
Step 6: Network Quality Assessment
- Run
runPreCallTest()as a pre-flight check before the user joins the room. - It verifies that the camera and microphone work, then measures how well the network carries a real call — uplink and downlink, audio and video — and returns a quality score along with the raw stats behind it.
Parameters
VideoSDK.runPreCallTest() takes a PreCallTestOptions and an optional onStatsChange 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 thennull. - 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. Pass that track, so the test does not open the camera a second time. - A track you pass in is never stopped for you, whatever the outcome — it stays yours to manage after the test.
- This is an
OPTIONALparameter.
- A camera
-
audioTrack:- A microphone
CustomStreamTrackyou have already created withcreateMicrophoneAudioTrack(). - A track you pass in is never stopped for you, whatever the outcome — 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
createRoom(). - This is an
OPTIONALparameter.
- A
-
audioConfig:- A
PreCallAudioConfigused to acquire the microphone track whenaudioTrackis not provided. - Accepts
microphoneId,encoderConfigandnoiseConfig. - This is an
OPTIONALparameter.
- A
-
onStatsChange:- A callback invoked while sampling runs. It receives the latest
PreCallNetworkQualityeach time the stats update. - This is an
OPTIONALparameter.
- A callback invoked while sampling runs. It receives the latest
Each of these rules fails the test with ERROR_PRECALL_INVALID_CONFIG, and the specific cause is preserved in the exception's message:
samplingDurationhas to be inside the 10000–120000 ms range.- Do not pass
videoTrackandvideoConfigtogether, and do not passaudioTrackandaudioConfigtogether — only one option from each pair is allowed. - Do not combine
audioOnly: truewithvideoTrackorvideoConfig.
For the camera's encoderConfig, use one of the presets listed in Custom Video Track. With other presets, the test or its camera check can fail on Android.
Run the test before the user joins the room. From the call to join() until that room is left, a new test is refused with ERROR_PRECALL_AFTER_INIT.
Creating a room does not stop a test that is already running. Call VideoSDK.stopPreCallTest() yourself before you create and join the room.
Example
runPreCallTest() returns a Future that completes with the PreCallTestResult when the test ends.
import 'package:videosdk/videosdk.dart';
try {
final result = await VideoSDK.runPreCallTest(
PreCallTestOptions(token: token, videoTrack: cameraTrack),
onStatsChange: (stats) => print(stats.uplink.quality),
);
print("camera ok: ${result.camera?.status}");
print("microphone ok: ${result.microphone?.status}");
print("downlink quality: ${result.networkQuality?.downlink.quality}");
// The test opened the microphone itself, so pass this track to createRoom() or stop it.
await result.microphone?.track?.stop();
} on PreCallTestException catch (error) {
print("Pre-call test failed: ${error.code} ${error.message}");
}
To stop a running test early, call stopPreCallTest(). The runPreCallTest() future still completes, with aborted set to true.
await VideoSDK.stopPreCallTest();
Result
runPreCallTest() completes with 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. null when audioOnly is true, or when the test was stopped before the device checks finished. |
microphone | PreCallMediaReport? | The microphone check. null when the test was stopped before the device checks finished. |
networkQuality | PreCallNetworkQuality? | The uplink and downlink measurements. null when the test was stopped. |
PreCallMediaReport carries status, track, captureResolution, fps, codec and error. If you passed videoTrack or audioTrack, track is that same track. If the test opened the device itself, from videoConfig or audioConfig or with the defaults when you passed neither, track is a live track that stays open after the test and is yours. Pass it to createRoom() as customCameraTrack or customMicrophoneTrack instead of creating another one, or call stop() on it.
PreCallNetworkQuality carries audioOnly plus uplink and downlink, each a PreCallDirectionQuality:
| Property | Type | Description |
|---|---|---|
quality | int | The score for that direction, from 1 (bad) to 5 (excellent), or 0 when unknown. |
factors | List<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
A failed test makes runPreCallTest() throw a PreCallTestException, which carries code and message. Note that code is a String here, not an integer.
| Code | When |
|---|---|
ERROR_PRECALL_INVALID_TOKEN | The token was empty. |
ERROR_PRECALL_INVALID_CONFIG | One of the option rules above was broken. |
ERROR_PRECALL_AFTER_INIT | join() was called, and that room has not been left yet. |
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 rejected the token. |
A device code, such as ERROR_INVALID_CUSTOM_VIDEO_TRACK or ERROR_MICROPHONE_IN_USE | A track you passed in could not be used, or, with audioOnly: true, the microphone could not be opened. |
When the test opens the devices itself and only one of them fails, the test still completes. That device's report has status set to false, and its error is a PreCallTestException with a code such as ERROR_CAMERA_IN_USE.
With these steps, your users pick their camera, microphone and speaker, and test their network, before they join a room.
API Reference
The API references for all the methods utilized in this guide are provided below.
Got a Question? Ask us on discord

