Skip to main content
Version: 3.x.x

Precall Setup - Android

A precall screen lets users check their camera, microphone and network before they join a call.

Why is it necessary?​

Without it, users can join a call with a muted microphone, the wrong camera or a poor connection, and find out only once the call has started.

A precall step lets them fix these problems before they join.

Step-by-Step Guide: Integrating Precall Feature​

Step 1: Check and Request Permissions​

  • Your app needs the camera, microphone and, for Bluetooth headsets, Bluetooth permissions before the precall screen opens any device.
  • Call VideoSDK.checkPermissions(). It asks the user for any permission that is missing, and reports the result on the PermissionHandler you pass. Exactly one of its callbacks is called:
    • onGranted(): every requested permission is granted.
    • onBlocked(): one or more permissions were already blocked before this request (the user had checked "Don't ask again"). Return true if you handle it yourself, or false to let the SDK show a dialog that opens the app's settings.
    • onDenied(): one or more permissions were denied, and the system dialog can still be shown again later.
    • onJustBlocked(): the user checked "Don't ask again" during this request. By default it calls onDenied().
  • When every permission is already granted, onGranted() is called right away on the calling thread. Every other result arrives on the main thread.
  • The first request goes straight to the system prompt. The rationale dialog appears only for a permission the user has denied before.
  • For a blocked permission, the SDK's go-to-settings dialog (settingsDialogTitle, settingsDialogMessage, settingsText) appears only when onBlocked() returns false (the default) and options.sendBlockedToSettings is true (the default).
  • When the user comes back from Settings, the SDK checks again. A permission that is still blocked shows the dialog again.
note

Declare the permissions you request in your app's AndroidManifest.xml: RECORD_AUDIO and MODIFY_AUDIO_SETTINGS for Permission.audio, CAMERA for Permission.video, and BLUETOOTH_CONNECT for Permission.bluetooth (Android 12 and later; on older versions it asks for nothing). The SDK doesn't add them to your manifest, and Android refuses a permission that isn't declared without showing a dialog.

private fun checkPermissions() {
val permissionList: MutableList<Permission> = ArrayList()
permissionList.add(Permission.audio)
permissionList.add(Permission.video)
permissionList.add(Permission.bluetooth)
val options = Permissions.Options().apply {
rationaleDialogTitle = "Info"
settingsDialogTitle = "Warning"
}
VideoSDK.checkPermissions(
this,
permissionList,
rationale = "Camera and microphone access is needed for the call",
options = options,
permissionHandler = permissionHandler,
)
}

private val permissionHandler: PermissionHandler = object : PermissionHandler() {
override fun onGranted() {
Log.d("VideoSDK Permission", "onGranted: All permissions granted")
}
override fun onBlocked(
context: Context,
blockedList: java.util.ArrayList<Permission>
): Boolean {
for (blockedPermission in blockedList) {
Log.d("VideoSDK Permission", "onBlocked: $blockedPermission")
}
return super.onBlocked(context, blockedList)
}

override fun onDenied(
context: Context,
deniedPermissions: java.util.ArrayList<Permission>
) {
for (deniedPermission in deniedPermissions) {
Log.d("VideoSDK Permission", "onDenied: $deniedPermission")
}
super.onDenied(context, deniedPermissions)
}

override fun onJustBlocked(
context: Context,
justBlockedList: java.util.ArrayList<Permission>,
deniedPermissions: java.util.ArrayList<Permission>
) {
for (justBlockedPermission in justBlockedList) {
Log.d("VideoSDK Permission", "onJustBlocked: $justBlockedPermission")
}
super.onJustBlocked(context, justBlockedList, deniedPermissions)
}
}

Step 2: Render Device Lists​

  • You can fetch and render lists of available cameras and audio devices using the getCameras() and getMics() methods of the VideoSDK class, respectively.
  • Both lists are empty until VideoSDK.initialize() has run.
  • getMics() lists audio routes, such as the speaker, the earpiece, a wired headset or a Bluetooth headset, not individual microphones. Switching to a headset or Bluetooth route also switches to its microphone.
import live.videosdk.rtc.android.mediaDevice.CameraDeviceInfo
import live.videosdk.rtc.android.mediaDevice.MicrophoneDeviceInfo

private fun getMics() {
val mics: List<MicrophoneDeviceInfo> = VideoSDK.getMics()
for (mic in mics) {
Log.d("VideoSDK", "Audio Device's DeviceId " + mic.deviceId)
Log.d("VideoSDK", "Audio Device's Label " + mic.label)
Log.d("VideoSDK", "Audio Device's Kind " + mic.kind)
}
}

private fun getCameras() {
val cameras: List<CameraDeviceInfo> = VideoSDK.getCameras()
for (camera in cameras) {
Log.d("VideoSDK", "Camera's DeviceId " + camera.deviceId)
Log.d("VideoSDK", "Camera's Label " + camera.label)
Log.d("VideoSDK", "Camera's Kind " + camera.kind)
}
}

Step 3: Handle Audio Device Changes​

  • Implement the onAudioDeviceChanged() callback of AudioDeviceChangeEvent and register it with the setAudioDeviceChangeListener() method of the Room class to dynamically re-render audio device lists whenever new devices are attached or removed from the system.
  • createRoom() does not connect anything, so you can create the room for the precall screen and register the listener before join(). The registration stays in place after the room is left, until you replace it or pass null.
  • If you also run the network test from Step 6, run it before you create the room.
  • The callback is called once as soon as you register it, with the current devices, and again on every change.
  • Update your device list in the callback, so users can pick newly connected devices.
import live.videosdk.rtc.android.mediaDevice.MicrophoneDeviceInfo

room.setAudioDeviceChangeListener { selectedAudioDevice: MicrophoneDeviceInfo?, audioDevices: List<MicrophoneDeviceInfo> ->
Log.d("VideoSDK", "Selected Audio Device: " + selectedAudioDevice?.label)
for (audioDevice in audioDevices) {
Log.d("VideoSDK", "Audio Devices" + audioDevice.label)
}
}

Step 4: Ensuring the selected devices are used in the Room​

  • Ensure that the status of all relevant devices, such as the microphone and camera (on/off), as well as the selected devices, are correctly configured and passed into the room from the precall screen.
  • To select a camera, create a custom track with the chosen camera's deviceId and pass it to createRoom() as customCameraTrack. The setSelectedVideoDevice() method of the VideoSDK class only records your choice: it doesn't change the camera the room opens.
  • createCameraVideoTrack() is a suspend function. It returns once the camera is open and throws VideoSDKException if it cannot be opened.
  • To show the camera on the precall screen, add the track to a VideoView with videoView.addTrack(cameraTrack.track as? VideoTrack), where VideoTrack is videosdk.org.webrtc.VideoTrack. The room then publishes the same camera without opening it again.
  • To select an audio device, use the setAudioDevice() method of the Room class. It works before join() too.
  • setAudioDevice() throws VideoSDKException with 3059 (ERROR_NO_MICROPHONE_DEVICE_FOUND) when the device is not one of the routes VideoSDK.getMics() lists right now, for example a headset that was unplugged. The error is also reported on onError, and the audio route stays as it was.
  • This way, users keep the settings they chose on the precall screen when they join the room.
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Specify the camera to use in the room
val camera: CameraDeviceInfo = selectedCamera

// Specify the audio device to use in the room
val mic: MicrophoneDeviceInfo = selectedMic

lifecycleScope.launch {
var cameraTrack: CustomStreamTrack? = null
try {
cameraTrack = VideoSDK.createCameraVideoTrack(
encoderConfig = CustomCameraTrackConfig.H720P_W960P,
multiStream = true,
cameraId = camera.deviceId,
)
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Camera track failed: ${e.code} ${e.name}")
}

VideoSDK.setSelectedVideoDevice(camera)

val room: Room = VideoSDK.createRoom(
roomId = "roomId",
token = "YOUR_TOKEN",
participantName = "John Doe",
micEnabled = true,
camEnabled = true,
multiStream = false,
customCameraTrack = cameraTrack,
)

try {
room.setAudioDevice(mic)
} catch (e: VideoSDKException) {
// 3059: the selected device is no longer available
Log.e("VideoSDK", "Audio device failed: ${e.code} ${e.name}")
}
}
}
}

Step 5: Get Selected Devices​

  • getSelectedVideoDevice() and getSelectedAudioDevice() of the VideoSDK class return the video and audio devices that are currently selected.
  • Both return null when nothing is selected yet: getSelectedVideoDevice() before a camera has been opened or selected, and getSelectedAudioDevice() before VideoSDK.initialize() has run.
private fun getSelectedVideoDevice(){
Log.d("TAG", "onCreate: " + VideoSDK.getSelectedVideoDevice()?.label)
}

private fun getSelectedAudioDevice(){
Log.d("TAG", "onCreate: " + VideoSDK.getSelectedAudioDevice()?.label)
}

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

VideoSDK.initialize(context) must have run first, or the call throws IllegalStateException.

The test never requests the CAMERA or RECORD_AUDIO permissions — request them yourself before starting it (see Step 1).

Run the test before you call createRoom(). Once a room is created, the test fails with ERROR_PRECALL_AFTER_INIT until that room is left.

Parameters​

runPreCallTest() takes a PreCallTestOptions and an optional onStatsChange function.

  • token:

    • The authentication token used to authorize the test.
    • It has to be of String type.
    • This is a REQUIRED parameter.
  • samplingDuration:

    • Controls how long (in milliseconds) the network-stats phase runs.
    • It has to be of Int type.
    • This is an OPTIONAL parameter.
    • Default: 15000. A value outside 10000 to 120000 fails the test with ERROR_PRECALL_INVALID_CONFIG.
  • audioOnly:

    • Skips the camera test and runs the pre-call test using audio only; the result's camera field is then null.
    • It has to be of Boolean type.
    • This is an OPTIONAL parameter.
    • Default: false
  • videoTrack:

    • A camera CustomStreamTrack you have already created, such as the one rendered on your precall screen.
    • A track you pass in is never stopped for you — it stays yours to manage after the test.
    • This is an OPTIONAL parameter.
  • audioTrack:

    • A microphone CustomStreamTrack you have already created.
    • A track you pass in is never stopped for you — it stays yours to manage after the test.
    • This is an OPTIONAL parameter.
  • videoConfig:

    • A PreCallVideoConfig used to acquire the camera track when videoTrack is not provided.
    • Accepts cameraId, encoderConfig (a CustomCameraTrackConfig), facingMode, multiStream, bitrateMode and maxLayer (an EncodingLayer).
    • This is an OPTIONAL parameter.
  • audioConfig:

    • A PreCallAudioConfig used to acquire the microphone track when audioTrack is not provided.
    • Accepts microphoneId, encoderConfig (a CustomMicrophoneTrackConfig) and noiseConfig.
    • This is an OPTIONAL parameter.
  • onStatsChange:

    • Called on the main thread with each PreCallNetworkQuality sample while the test runs.
    • This is an OPTIONAL parameter. Pass null, or leave it out, for no samples.

Passing both videoTrack and videoConfig, both audioTrack and audioConfig, or audioOnly = true with videoTrack or videoConfig fails the test with ERROR_PRECALL_INVALID_CONFIG.

caution

Without a videoTrack / audioTrack the test opens the devices itself, exactly as createCameraVideoTrack() does — so an open camera preview on another track ends when the test starts.

After a successful test those tracks stay open and belong to your app, reachable through result.camera?.track and result.microphone?.track. Either publish them into the room or stop them yourself.

Example​

runPreCallTest() is a suspend function. It returns the PreCallTestResult when the test finishes and throws PreCallTestException on failure.

val options = PreCallTestOptions(
token = token,
samplingDuration = 15000,
videoConfig = PreCallVideoConfig(encoderConfig = CustomCameraTrackConfig.H720P_W1280P),
audioConfig = PreCallAudioConfig(encoderConfig = CustomMicrophoneTrackConfig.SPEECH_STANDARD),
)

val testJob = lifecycleScope.launch {
try {
val result = VideoSDK.runPreCallTest(options) { stats ->
Log.d("#precall", "uplink quality: ${stats.uplink?.quality}")
}
Log.d("#precall", "camera ok: ${result.camera?.status}")
Log.d("#precall", "downlink quality: ${result.networkQuality?.downlink?.quality}")
} catch (error: PreCallTestException) {
Log.e("#precall", "${error.code}: ${error.message}")
}
}

To stop a running test early, cancel the coroutine, or call stopPreCallTest():

testJob.cancel()
// or, without holding the job:
VideoSDK.stopPreCallTest()
note

After stopPreCallTest(), the suspended runPreCallTest() call still returns its result. Joining a room stops a running test.

Result​

PreCallTestResult carries:

PropertyTypeDescription
abortedBooleantrue when the test was stopped before it finished.
testDurationLongHow long the test actually ran, in milliseconds.
cameraPreCallMediaReport?The camera check. null when audioOnly is true.
microphonePreCallMediaReport?The microphone check.
networkQualityPreCallNetworkQuality?The uplink and downlink measurements.

PreCallMediaReport carries status, track, captureResolution, fps, codec and error.

PreCallNetworkQuality carries audioOnly plus uplink and downlink, each a nullable PreCallDirectionQuality:

PropertyTypeDescription
qualityIntThe score for that direction, from 1 (bad) to 5 (excellent), or 0 when unknown.
factorsList<String>What limited the score.
audioPreCallAudioQuality?quality, rtt, bitrate, packetLoss, jitter, bytesSent, bytesReceived.
videoPreCallVideoQuality?The audio fields plus fps, resolution, qualityLimitationReason, framesDropped, freezeCount, framesDroppedRatio and totalFreezesDuration.

Not every field is filled in both directions. On uplink, jitter is null, and so are the receive-side video fields framesDropped, freezeCount, framesDroppedRatio and totalFreezesDuration. Read them from downlink.

Errors​

Failures arrive as PreCallTestException, which carries code and message. Note that code is a String here, not an integer.

CodeWhen
ERROR_PRECALL_INVALID_TOKENThe token was empty.
ERROR_PRECALL_INVALID_CONFIGThe options were not a valid combination, or samplingDuration was outside 10000 to 120000.
ERROR_PRECALL_TEST_ALREADY_RUNNINGAnother test is still running.
ERROR_PRECALL_MEDIA_CHECK_FAILEDNeither the camera nor the microphone could be opened.
ERROR_PRECALL_AFTER_INITA room was created with createRoom() and has not been left yet.
ERROR_PRECALL_TEST_FAILEDThe test ended without producing a result, for example because the server rejected the token. A token that is not empty but invalid fails with this code.

If only one device fails, the test still completes. That device's PreCallMediaReport has status set to false and an error with a code such as ERROR_CAMERA_IN_USE.

API Reference​

The API references for all the methods utilized in this guide are provided below.

Got a Question? Ask us on discord