Skip to main content
Version: 3.x.x

Coroutines and Error Handling - Android

Most actions in the Android SDK are Kotlin suspend functions. This includes join(), leave(), the mic, camera and screen share controls, recording and livestream controls, the PubSub methods, and the actions on a Participant or an RTCStream. This guide shows how to call them and how to handle the errors they report.

Calling actions from a coroutine​

A suspend function can only be called from a coroutine. Launch one from a scope that matches the screen or class that owns the room:

  • In an Activity or Fragment, use lifecycleScope, from androidx.lifecycle:lifecycle-runtime-ktx.
  • In a ViewModel, use viewModelScope, from androidx.lifecycle:lifecycle-viewmodel-ktx.
  • In Jetpack Compose, use the scope from rememberCoroutineScope().

The SDK brings kotlinx-coroutines-android with it, so you only need to add the library that provides the scope.

app/build.gradle.kts
dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
// only if you call the SDK from a ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
}

You can call these functions from the main thread. They don't block it while they wait for the SDK.

class RoomActivity : AppCompatActivity() {
private lateinit var room: Room

private fun toggleMic() {
lifecycleScope.launch {
try {
if (room.isMicEnabled) room.muteMic() else room.unmuteMic()
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Mic toggle failed: ${e.code} ${e.name}")
}
}
}
}

In a ViewModel, the same call looks like this:

class RoomViewModel(private val room: Room) : ViewModel() {
fun toggleCam() {
viewModelScope.launch {
try {
if (room.isCamEnabled) room.disableCam() else room.enableCam()
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Camera toggle failed: ${e.code} ${e.name}")
}
}
}
}
note
  • A suspend function returns when the SDK has handled your request. A request that the server carries out, such as startRecording(), can return before the work is done. Wait for its event, for example onRecordingStateChanged(), before you update the UI.
  • A scope is cancelled when its owner goes away: lifecycleScope when the activity is destroyed, viewModelScope when the ViewModel is cleared. A coroutine launched from onDestroy() never runs. Leave the room before the screen closes, as shown in Leave or End Room.

The track factories VideoSDK.createCameraVideoTrack() and VideoSDK.createMicrophoneAudioTrack() are also suspend functions. They return once the device is open, and throw a VideoSDKException if it cannot be opened.

The PubSub methods publish(), subscribe() and unsubscribe() are suspend functions too. They return once the server has answered, and throw a VideoSDKException on failure.

Some calls are not suspend functions but can still throw VideoSDKException, for example VideoSDK.createRoom() and room.setAudioDevice(). Wrap them in try/catch too.

Handling VideoSDKException​

When an action is refused or fails, it throws a VideoSDKException. It is an unchecked exception, so the compiler does not remind you to catch it. An exception you don't catch crashes the app.

VideoSDKException has these properties:

PropertyTypeDescription
codeIntThe error code, for example 3054.
nameStringThe error name, for example "ERROR_ALREADY_IN_REQUESTED_STATE".
messageString?A readable reason. Log it, but don't branch on it, because the server can change the text.
errorVideoSDKErrorThe failure as a VideoSDKError, with this exception's code, name and message.

Branch on error, or on code, to decide what to do. Don't branch on name: names can change between releases. A VideoSDKError equals its constant when the codes match, so compare with == or when, never with ===. A when used as an expression needs an else branch.

lifecycleScope.launch {
try {
room.enableCam()
} catch (e: VideoSDKException) {
when (e.error) {
VideoSDKError.ERROR_ALREADY_IN_REQUESTED_STATE -> Unit // the camera is already on
VideoSDKError.ERROR_OPERATION_IN_PROGRESS -> Unit // a camera toggle is still running
VideoSDKError.ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED -> showPermissionHelp()
else -> Log.e("VideoSDK", "Camera failed: ${e.code} ${e.name} ${e.message}")
}
}
}

Errors reported on onError​

Errors also reach the onError() event of RoomEventListener as a VideoSDKError with code, name and message. It equals its VideoSDKError constant when the codes match, so you can branch on it with when. Add an else branch for every other error.

private val roomEventListener = object : RoomEventListener() {
override fun onError(error: VideoSDKError) {
when (error) {
VideoSDKError.ERROR_ROOM_RECONNECTING -> Unit // wait for onRoomStateChanged
else -> Log.e("VideoSDK", "onError: ${error.code} ${error.name} ${error.message}")
}

// onError can arrive off the main thread
runOnUiThread { showError(error.message) }
}
}

An error can reach your app in three ways:

  • Thrown and reported. Most errors are thrown by the call that failed and also reported on onError.
  • Thrown only. Some errors describe a mistake in the call itself and are only thrown, for example 3048 (ERROR_MEDIA_NOT_READY), 3066 (ERROR_ACTION_NOT_SUPPORTED_IN_MODE), 3072 (ERROR_PAYLOAD_TOO_LARGE) and 3073 (ERROR_INVALID_PARAMETER). Catch them where you make the call.
  • Reported only. Problems that happen after a call has returned, such as a camera that stops working during the call or a recording that fails on the server, are only reported on onError.

Calls made before joining or while reconnecting​

Most actions need a joined room. Called before join() completes, they report 3035 (ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED) on onError. Called while the room is reconnecting, they report 3065 (ERROR_ROOM_RECONNECTING). Either way, nothing is sent and nothing changes. Participant methods are the exception during a join: called while join() is still running, they wait for it to finish instead of reporting 3035.

Many actions, such as unmuteMic(), enableCam() and startRecording(), then return without throwing. Others also throw a VideoSDKException with the same code, for example changeMic(), changeCam(), uploadBase64File(), fetchBase64File(), the PubSub methods and RTCStream.pause() / resume(). While the room is reconnecting, pubSub.publish() and pubSub.subscribe() throw 3065 without reporting it on onError. Each method's API reference lists what it throws.

A call that is refused while the room reconnects is not repeated afterwards. Make the call again once onRoomStateChanged() reports CONNECTED.

Threads​

  • RoomEventListener, ParticipantEventListener and PubSubMessageListener callbacks run on the main thread, so you can update views from them. One exception: when you call participant.addEventListener(), onStreamEnabled() is called right away for each stream the participant already has, on the thread that registers the listener.
  • onError() is the exception: an error raised by a refused call arrives on the thread that made the call, and a camera or microphone failure arrives on a background thread. Post to the main thread before you update the UI.
  • Catch and log exceptions inside your own listener callbacks. Otherwise a bug in a callback can go unnoticed.

API Reference​

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

Got a Question? Ask us on discord