Skip to main content
Version: 3.x.x

Migration Guide

This guide covers what you need to change when upgrading the Android SDK from 2.2.0 to 3.0.0. Version 3.0.0 is not a drop-in upgrade: the meeting is now a Room, most actions are suspend functions that throw, many callbacks and parameters are typed, and many error codes were renumbered.

Update the dependency first:

implementation("live.videosdk:rtc-android-sdk:3.0.0")
caution
  • Kotlin only. Java callers are not supported, so the code that calls the SDK must be in Kotlin, built with Kotlin 1.8 or later.

  • Android 7.0 (API 24) or later. Raise minSdkVersion from 21 to 24.

  • ARM only. The SDK runs on arm64-v8a and armeabi-v7a. x86 / x86_64 emulator images are no longer supported: test on an ARM phone or an arm64-v8a emulator image. If your app ships native libraries for other ABIs, set ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' } so the app is not installed where the SDK cannot load.

  • Declare these permissions yourself. 2.2.0 added them to your manifest. 3.0.0 does not.

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
  • Fewer libraries come with the SDK. It no longer brings in OkHttp, RxJava, Socket.IO, OpenTelemetry, Wire, Timber, mediasoup, the org.webrtc classes, or AppCompat and Material Components with the AndroidX libraries they pull in, such as ConstraintLayout, RecyclerView and Fragment. Declare any of them your app uses, including ones it only names in XML layouts and themes. A library that depends on an older version of one of them, such as OkHttp 3, now gets that version instead of the newer one 2.2.0 brought in: declare the newer version yourself to keep it. kotlinx-coroutines-android now comes with the SDK. The examples below launch coroutines with lifecycleScope, which comes from androidx.lifecycle:lifecycle-runtime-ktx: add it if your app doesn't already have it. If your app keeps the Room in a ViewModel, use viewModelScope instead.

  • ProGuard / R8. The SDK ships keep rules for its own API. If you minify your app, also add the rules below for the SDK's videosdk.org classes, which include the WebRTC classes. The WebRTC classes moved from org.webrtc to videosdk.org.webrtc (see section 9), so rules you wrote for org.webrtc.** no longer match anything: replace them with these. Keep the -dontwarn line, because the keep rule alone fails the R8 step with a missing-class error.

    proguard-rules.pro
    -keep class videosdk.org.** { *; }
    -dontwarn videosdk.org.**

Changes the compiler will find​

1. The meeting is now a room​

VideoSDK.config(token) and VideoSDK.initMeeting(…) are replaced by one call, VideoSDK.createRoom(roomId, token, …), which returns a Room. Every name that said "meeting" for the room now says "room": Meeting is Room, MeetingEventListener is RoomEventListener, and meetingId is roomId. The webcam methods are now enableCam(), disableCam() and changeCam().

Before

VideoSDK.initialize(applicationContext)
VideoSDK.config(token)
val meeting: Meeting = VideoSDK.initMeeting(
this, meetingId, "John Doe",
true, true, null, null, false, null, null
)
meeting.addEventListener(meetingEventListener)
meeting.join()

After

import live.videosdk.rtc.android.Room
import live.videosdk.rtc.android.listeners.RoomEventListener

VideoSDK.initialize(applicationContext)
val room: Room = VideoSDK.createRoom(
roomId = roomId,
token = token,
participantName = "John Doe",
micEnabled = true,
camEnabled = true,
multiStream = false,
)
room.addEventListener(roomEventListener)
lifecycleScope.launch {
try {
room.join()
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Join failed: ${e.code} ${e.name}")
}
}

Key points:

  • createRoom takes no context. Call VideoSDK.initialize(context) once first, for example in Application.onCreate(). Without it, createRoom and the camera and microphone track factories throw IllegalStateException.
  • The token is a createRoom parameter. An empty token or roomId throws VideoSDKException 3073 (ERROR_INVALID_PARAMETER). A token the server rejects makes join() throw the server's code, such as 4002 (INVALID_TOKEN).
  • The parameters, in order: roomId, token, participantName, micEnabled, camEnabled, participantId, mode, multiStream, customCameraTrack, customMicrophoneTrack, metaData, signalingBaseUrl, preferredProtocol, codecSwitchEnabled, enableBatching, then the new translationLanguage, speakingLanguage, maxResolution, defaultCamera, autoConsume and dynaCast (see New in 3.0.0). Use named arguments.
  • mode is a Mode (Mode.SEND_AND_RECV, Mode.SIGNALLING_ONLY, Mode.RECV_ONLY), not a string. null means SEND_AND_RECV. The old "CONFERENCE" and "VIEWER" strings are gone: use SEND_AND_RECV and SIGNALLING_ONLY.
  • The customTracks map is now two parameters: customCameraTrack (was the "video" entry) and customMicrophoneTrack (was the "mic" entry).
  • multiStream = null means true, and maxResolution = null means MaxResolution.hd.
  • participantName = null joins with a random six-character name, as in 2.2.0. An empty name now does too, where 2.2.0 joined with an empty name. The local participant's displayName is set once onRoomJoined fires.
  • Room.activeRoom() replaces Meeting.activeMeeting(). It returns the room that is joined or joining, or null.

The Room members that were renamed:

2.2.0 Meeting3.0.0 Room
getMeetingId()id
getMeetingState()roomState: RoomState
isE2eeEnabled()e2eeEnabled
removeAllListeners()removeAllEventListeners()
enableWebcam(), enableWebcam(customTrack)enableCam(), enableCam(customCameraTrack)
disableWebcam()disableCam()
changeWebcam(), changeWebcam(deviceId)changeCam(), changeCam(deviceId), and the new changeCam(customCameraTrack)
unmuteMic(customTrack)unmuteMic(customMicrophoneTrack)
startHls(config, transcription), stopHls()startHLS(config: HLSConfig? = null, transcription = null), stopHLS()
respondEntry(pId, decision: String)respondEntry(participantId, decision: EntryDecision)
switchTo(meetingId, token)switchTo(roomId, token)
pauseAllStreams(kind: String), resumeAllStreams(kind: String)pauseAllStreams(kind: StreamKind? = null), resumeAllStreams(kind: StreamKind? = null)
requestMediaRelay(destinationMeetingId, token, userKinds: List<String>)requestMediaRelay(destinationRoomId, token = null, kinds: List<StreamKind>? = null)
stopMediaRelay(destinationMeetingId)stopMediaRelay(destinationRoomId)

2. Actions are suspend functions that throw VideoSDKException​

In 2.2.0, actions returned at once and reported failures only on onError. In 3.0.0, join(), leave(), the mic, camera and screen-share toggles and almost every other action on Room, Participant and RTCStream are suspend functions. They return when the call has finished and throw VideoSDKException when it is refused or fails. Call them from a coroutine and catch the exception. Recording, livestream, HLS and transcription calls return once the request is sent, so keep waiting for their state events (onTranscriptionStateChanged for transcription).

Before

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
meeting.addEventListener(meetingEventListener)
meeting.join()
}

fun toggleMic() {
if (micEnabled) meeting.muteMic() else meeting.unmuteMic()
micEnabled = !micEnabled
}

After

import live.videosdk.rtc.android.lib.VideoSDKException

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
room.addEventListener(roomEventListener)
lifecycleScope.launch {
try {
room.join()
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Join failed: ${e.code} ${e.name} ${e.message}")
}
}
}

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}")
}
// read the state back instead of flipping your own flag
micEnabled = room.isMicEnabled
}
}

Key points:

  • VideoSDKException is unchecked. One you don't catch crashes the app like any other uncaught exception.
  • It carries code and name, and error returns the matching VideoSDKError (see section 4).
  • join() returns once the mic and camera you asked for in createRoom have started or failed, so onRoomJoined can fire before join() returns. Set up anything onRoomJoined reads, such as the field that holds the room, before you call join().
  • The PubSub calls publish, subscribe and unsubscribe are suspend functions too (see section 8).
  • These are still plain functions: realtimeStore.observe and stopObserving, room.setAudioDeviceChangeListener, room.setAudioDevice, room.setVideoProcessor and room.removeVideoProcessor.
  • Called before the room is joined, or while it is reconnecting, most actions still report 3035 / 3065 on onError and return without throwing. changeCam, changeMic, uploadBase64File, fetchBase64File, the PubSub calls and RTCStream.pause / resume report the error and also throw it, except that the PubSub calls only throw 3065. RealtimeStore.setValue / getValue and room.send throw both codes, but only 3035 also reaches onError. end() throws both codes. leave() only throws before a join, and still leaves while the room is reconnecting (see section 18).
  • Drive your mic and camera buttons from room.isMicEnabled and room.isCamEnabled, not from a flag you flip yourself. A toggle refused before the join completes doesn't throw, and a mic or camera that failed to start stays off, so your own flag drifts from the device. After that, every tap calls the wrong function and throws 3054.

3. RoomEventListener callbacks changed​

MeetingEventListener is now RoomEventListener, and many callbacks were renamed or now pass typed values instead of JSONObject. The overloads without a reason or room id are gone, and every parameter has fixed Kotlin nullability. An override that no longer matches fails with "overrides nothing".

Before

private val meetingEventListener = object : MeetingEventListener() {
override fun onMeetingJoined() {
Log.d("VideoSDK", "Joined")
}

override fun onMeetingLeft() {
Log.d("VideoSDK", "Left")
}

override fun onEntryRequested(id: String, name: String) {
meeting.respondEntry(id, "allowed")
}

override fun onHlsStateChanged(HlsState: JSONObject) {
if (HlsState.getString("status") == "HLS_PLAYABLE") {
play(HlsState.getString("playbackHlsUrl"))
}
}
}

After

import live.videosdk.rtc.android.lib.HLSState
import live.videosdk.rtc.android.lib.LeaveReason
import live.videosdk.rtc.android.listeners.RoomEventListener
import live.videosdk.rtc.android.model.HLSUrl

private val roomEventListener = object : RoomEventListener() {
override fun onRoomJoined(switchRoomId: String?) {
Log.d("VideoSDK", "Joined")
}

override fun onRoomLeft(reason: LeaveReason) {
Log.d("VideoSDK", "Left: ${reason.message}")
}

override fun onEntryRequested(participantId: String, name: String, allow: () -> Unit, deny: () -> Unit) {
allow()
}

override fun onHlsStateChanged(state: HLSState, hlsUrl: HLSUrl?) {
if (state == HLSState.HLS_PLAYABLE) {
hlsUrl?.playbackHlsUrl?.let { play(it) }
}
}
}
2.2.0 MeetingEventListener3.0.0 RoomEventListener
onMeetingJoined(), onMeetingJoined(switchRoomId)onRoomJoined(switchRoomId: String?)
onMeetingLeft(), onMeetingLeft(reason)onRoomLeft(reason: LeaveReason)
onMeetingStateChanged(state: MeetingState)onRoomStateChanged(roomState: RoomState)
onParticipantLeft(participant)onParticipantLeft(participant, reason: LeaveReason)
onPresenterChanged(participantId)onPresenterChanged(presenterId: String?)
onSpeakerChanged(participantId)onSpeakerChanged(activeSpeakerId: String?)
onEntryRequested(id, name)onEntryRequested(participantId, name, allow: () -> Unit, deny: () -> Unit)
onEntryResponded(id, decision: String)onEntryResponded(participantId, decision: EntryDecision?)
onRecordingStateChanged(recordingState: String)onRecordingStateChanged(state: RecordingState)
onLivestreamStateChanged(livestreamState: String)onLivestreamStateChanged(state: LiveStreamState)
onHlsStateChanged(HlsState: JSONObject)onHlsStateChanged(state: HLSState, hlsUrl: HLSUrl?)
onHlsStarted(), onHlsStopped()onHlsStateChanged with HLS_STARTED / HLS_STOPPED
onTranscriptionStateChanged(data: JSONObject)onTranscriptionStateChanged(state: TranscriptionState)
onParticipantModeChanged(data: JSONObject)onParticipantModeChanged(participantId: String, mode: Mode)
onPinStateChanged(pinStateData: JSONObject)onPinStateChanged(participantId: String, state: ParticipantPinState, pinnedBy: String?)
onError(error: JSONObject)onError(error: VideoSDKError) (see section 4)
onExternalCallRinging()onExternalCallRinging(callType: CallType)
onExternalCallStarted()onExternalCallAnswered(callType: CallType)
onExternalCallHangup()onExternalCallHangup(callType: CallType) for an answered call, and the new onExternalCallRejected(callType: CallType) for a ringing call that ended unanswered
onPausedAllStreams(kind: String), onResumedAllStreams(kind: String)onPausedAllStreams(kind: StreamKind?), onResumedAllStreams(kind: StreamKind?)
onMediaRelayRequestReceived(participantId, liveStreamId, displayName, listener: RelayRequestListener)onMediaRelayRequestReceived(participantId, roomId, displayName, accept: () -> Unit, reject: () -> Unit)
onMediaRelayRequestResponse(decidedBy, decision: String)onMediaRelayRequestResponse(decision: RelayDecision?, decidedBy, roomId). decision now comes first
onMediaRelayStarted(meetingId), onMediaRelayStopped(meetingId, reason), onMediaRelayError(meetingId, reason)onMediaRelayStarted(roomId), onMediaRelayStopped(roomId, reason: String?), onMediaRelayError(roomId, error). reason is null when the server gave none
onQualityLimitation(type, state)onQualityLimitation(type, state, timestamp: Long)
onMainParticipantChanged(participant)Removed
onCharacterJoined(character), onCharacterLeft(character)Removed

Key points:

  • onRoomJoined(switchRoomId) is the only join callback. It fires with null on every join, with the new room id after switchTo(), and again after a reconnect that needed a fresh join. Don't use it for one-time setup.
  • onEntryRequested hands you allow and deny: call one of them to answer. room.respondEntry(participantId, EntryDecision.ALLOWED) or EntryDecision.DENIED still works if you answer later.
  • callType in the external-call callbacks is a CallType: INCOMING or OUTGOING. Android cannot tell a declined call from a missed one: both arrive as onExternalCallRejected.
  • HLSUrl carries playbackHlsUrl and livestreamUrl. It is non-null when the server sent a URL, in practice with HLS_PLAYABLE. There is no downstreamUrl.
  • Nullable: onRoomJoined's switchRoomId, onPresenterChanged's presenterId, onSpeakerChanged's activeSpeakerId, onPinStateChanged's pinnedBy, onHlsStateChanged's hlsUrl, onMediaRelayStopped's reason, the decision of onEntryResponded and onMediaRelayRequestResponse, the kind of onPausedAllStreams, onResumedAllStreams, onStreamPaused and onStreamResumed, and the selected device in AudioDeviceChangeEvent. Everything else is non-null.
  • RealtimeStoreListener.onValueChanged(newValue: String?, updatedBy: Participant?). A cleared key arrives as null, and updatedBy is null when the writer has left or was never seen by this device.
  • onRecordingStateChanged and onLivestreamStateChanged pass a RecordingState (RECORDING_STARTING … RECORDING_STOPPED) and a LiveStreamState (LIVESTREAM_STARTING … LIVESTREAM_STOPPED) instead of a string. Compare with the enum.
  • onMediaRelayRequestResponse passes a RelayDecision (ACCEPTED or REJECTED) first and gained a roomId, so a 2.2.0 override no longer compiles: rewrite it in the new order.
  • Every callback of RoomEventListener and ParticipantEventListener has an empty default, so object : RoomEventListener() { … } overrides only what you need.

4. Errors are VideoSDKError values​

onError now passes a VideoSDKError with code, name and message, not a JSONObject. VideoSDKError is a class with a named constant for each error, such as VideoSDKError.INVALID_TOKEN. Constants that said "meeting" now say "room" (table below). Two errors are equal when their codes are equal.

Before

override fun onError(error: JSONObject) {
val code = error.optInt("code")
if (code == 4002) refreshToken()
Log.e("VideoSDK", error.optString("message"))
}

After

override fun onError(error: VideoSDKError) {
when (error) {
VideoSDKError.INVALID_TOKEN -> refreshToken()
else -> Log.e("VideoSDK", "${error.code} ${error.name}: ${error.message}")
}
}

Key points:

  • Each error you receive is a new object. Compare it with == or a when, never with ===.
  • A when (error) used as an expression needs an else branch, because VideoSDKError is no longer an enum.
  • VideoSDKError.values() and toJson() are gone. Use VideoSDK.getErrorCodes() for the name-to-code map and VideoSDKError.fromCode(code) to look up one code.
  • VideoSDKException.error is a non-null VideoSDKError carrying the exception's message.
  • The SDK's own error and leave messages now say "room" instead of "meeting". Branch on codes, not on message text.

Renamed constants. Some also have a new number (see section 16):

2.2.03.0.0
ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED (3001)ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED (3035)
ERROR_MEETING_RECONNECTING (3014)ERROR_ROOM_RECONNECTING (3065)
INVALID_MEETING_ID (4003)INVALID_ROOM_ID (4003)
MAX_PARTICIPANT_REACHED (4009)MAX_PARTCIPANT_REACHED (4009)
UNAUTHORIZED_MEETING_ID (4022)UNAUTHORIZED_ROOM_ID (4022)
MEETING_ID_DISABLED (4052)ROOM_ID_DISABLED (4052)
SWITCH_MEETING_FAILED (4076)SWITCH_ROOM_FAILED (4076)

5. Track factories are typed, suspend and throw​

createAudioTrack is now createMicrophoneAudioTrack. Both camera and microphone factories are suspend functions that take no context, take typed encoder configs and return a non-null track. They throw VideoSDKException instead of returning null.

Before

val customTracks = mutableMapOf<String, CustomStreamTrack>()

val videoTrack: CustomStreamTrack? = VideoSDK.createCameraVideoTrack(
"h720p_w960p", "front", CustomStreamTrack.VideoMode.MOTION, true, context, null
)
if (videoTrack != null) customTracks["video"] = videoTrack

val audioTrack: CustomStreamTrack? = VideoSDK.createAudioTrack("high_quality", context)
if (audioTrack != null) customTracks["mic"] = audioTrack

val meeting = VideoSDK.initMeeting(context, meetingId, "John Doe", true, true, null, null, true, customTracks)

After

lifecycleScope.launch {
try {
val videoTrack = VideoSDK.createCameraVideoTrack(
encoderConfig = CustomCameraTrackConfig.H720P_W960P,
facingMode = FacingMode.front,
multiStream = true,
)
val audioTrack = VideoSDK.createMicrophoneAudioTrack(
encoderConfig = CustomMicrophoneTrackConfig.HIGH_QUALITY,
)
room = VideoSDK.createRoom(
roomId = roomId,
token = token,
participantName = "John Doe",
customCameraTrack = videoTrack,
customMicrophoneTrack = audioTrack,
)
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Track creation failed: ${e.code} ${e.name}")
}
}

Key points:

  • Encoder configs are enums: camera strings such as "h720p_w1280p" are CustomCameraTrackConfig.H720P_W1280P, and microphone strings such as "speech_standard" are CustomMicrophoneTrackConfig.SPEECH_STANDARD. The screen-share config is still a string.
  • createCameraVideoTrack takes cameraId (a deviceId from VideoSDK.getCameras()) instead of a VideoDeviceInfo, and the track reports it back as CustomStreamTrack.cameraId; maxLayer as an EncodingLayer (MAX_LAYER_2 or MAX_LAYER_3), and codec instead of videoCodec. For a single layer, pass multiStream = false. CustomStreamTrack.videoCodec is now codec.
  • The factories suspend until the device is open, whatever thread you call them from. The camera throws 3023 when another app holds it and 3026 for any other open failure. The microphone throws 3024.
  • Other errors: 3017 / 3018 when CAMERA / RECORD_AUDIO is not granted, 3021 / 3022 when there is no camera / microphone, and 3071 for an unknown camera or microphone id.
  • Before VideoSDK.initialize() has run, the factories throw IllegalStateException.
  • VideoSDK.maxLayer and VideoSDK.bitrateMode are gone. Pass maxLayer and bitrateMode to createCameraVideoTrack instead.
  • The optimizationMode argument and CustomStreamTrack.VideoMode are gone. Remove the argument.

6. Screen share: the track is returned, and audio is set on the track​

createScreenShareVideoTrack now returns the track instead of passing it to a CustomTrackListener, and it no longer takes a context. VideoSDK.CustomTrackListener is gone. enableScreenShare(track, enableAudio) became enableScreenShare(track): to share system audio, pass withAudio = true to createScreenShareVideoTrack.

Before

fun startScreenShare(data: Intent) {
VideoSDK.createScreenShareVideoTrack("h720p_15fps", data, this) { track ->
if (track != null) meeting.enableScreenShare(track, true)
}
}

After

fun startScreenShare(data: Intent) {
val track = VideoSDK.createScreenShareVideoTrack(
encoderConfig = "h720p_15fps",
data = data,
withAudio = true,
) ?: return
lifecycleScope.launch {
try {
room.enableScreenShare(track)
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Screen share failed: ${e.code} ${e.name}")
}
}
}

Key points:

  • createScreenShareVideoTrack returns at once. The track only carries the consent and the profile: capture and the foreground service start in enableScreenShare. 2.2.0 started the screen-share service at once, then started capturing and called the listener about two seconds later.
  • The track is null when the user declined the consent dialog, or when encoderConfig is not a known profile. The second case also reports 3071 (ERROR_INVALID_CUSTOM_TRACK_CONFIG) on onError.
  • encoderConfig defaults to "h720p_15fps", and multiStream to false.
  • room.enableScreenShare() with no track uses the consent from the last createScreenShareVideoTrack call, and throws 3048 (ERROR_MEDIA_NOT_READY) when there is none. Passing a camera track throws 3071.
  • The share notification and closing the app during a share behave differently too (see section 21).

7. Results are returned instead of passed to a callback​

TaskCompletionListener and RealtimeStoreCallback were removed. The calls that used them are now suspend functions that return the result and throw VideoSDKException on failure. RealtimeStore.set and get were also renamed to setValue and getValue.

Before

meeting.realtimeStore.set("score", "42", object : RealtimeStoreCallback<String> {
override fun onSuccess(result: String?) { Log.d("VideoSDK", "Saved $result") }
override fun onError(error: String?) { Log.e("VideoSDK", "Save failed: $error") }
})

meeting.uploadBase64File(base64Data, token, "notes.txt",
object : TaskCompletionListener<String, String> {
override fun onComplete(url: String?) { Log.d("VideoSDK", "Uploaded to $url") }
override fun onError(error: String?) { Log.e("VideoSDK", "Upload failed: $error") }
})

// height, then width
meeting.localParticipant.captureImage(720, 1280,
object : TaskCompletionListener<String, String> {
override fun onComplete(base64Image: String?) { Log.d("VideoSDK", "Captured") }
override fun onError(error: String?) { Log.e("VideoSDK", "Capture failed: $error") }
})

After

lifecycleScope.launch {
try {
room.realtimeStore.setValue("score", "42")
val score: String? = room.realtimeStore.getValue("score")

val url: String = room.uploadBase64File(base64Data, token, "notes.txt")
val file: String = room.fetchBase64File(url, token)

val base64Image: String = room.localParticipant.captureImage(width = 1280, height = 720)
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "${e.code} ${e.name}: ${e.message}")
}
}

Key points:

  • captureImage now takes the width first: captureImage(width = 0, height = 0, flash = CaptureFlash.OFF). A 2.2.0-style call that passes (height, width) by position still compiles and swaps them, so use named arguments.
  • uploadBase64File returns the uploaded file's URL, which is what fetchBase64File takes. fetchBase64File returns the file's base64 content.
  • captureImage returns a base64 JPEG. It throws 3062 (ERROR_WEBCAM_NOT_ENABLED) when the camera is off, where 2.2.0 threw java.lang.Error, and 3069 (ERROR_CAPTURE_IMAGE_FAILED) when the capture fails.
  • getValue on a key that was never set, or was cleared, throws 4046 (FAILED_TO_GET_VALUE), also reported on onError. Catch it where an unset key is normal. setValue(key, null) clears the key, and setValue returns nothing.

8. PubSub changes​

publish, subscribe and unsubscribe are suspend functions that throw VideoSDKException. publish is now one function with options before payload, so a four-argument call must swap its last two arguments or use named arguments. PubSubPublishOptions and PubSubSubscribeOptions are built with constructor arguments, the one-argument history callback is gone, and PubSubMessage.timestamp is a String.

Before

val publishOptions = PubSubPublishOptions()
publishOptions.isPersist = true
try {
meeting.pubSub.publish("CHAT", "Hello", payload, publishOptions)
} catch (e: IllegalStateException) {
Log.w("VideoSDK", "Not joined yet")
}

val subscribeOptions = PubSubSubscribeOptions()
subscribeOptions.oldMessageLimit = 50
meeting.pubSub.subscribe("CHAT", object : PubSubMessageListener {
override fun onMessageReceived(message: PubSubMessage) {
val text: String = message.message
val sentAt: Long = message.timestamp
Log.d("VideoSDK", "${message.senderName}: $text at $sentAt")
}

override fun onOldMessagesReceived(messages: List<PubSubMessage>) {
history.addAll(messages)
}
}, subscribeOptions)

After

val publishOptions = PubSubPublishOptions(persist = true)
val subscribeOptions = PubSubSubscribeOptions(oldMessageLimit = 50)

lifecycleScope.launch {
try {
room.pubSub.publish("CHAT", "Hello", publishOptions, payload)
room.pubSub.subscribe("CHAT", object : PubSubMessageListener {
override fun onMessageReceived(message: PubSubMessage) {
val text: String = message.message ?: ""
val sentAt: String = message.timestamp
Log.d("VideoSDK", "${message.senderName}: $text at $sentAt")
}

override fun onOldMessagesReceived(messages: List<PubSubMessage>, info: PubSubHistoryInfo) {
history.addAll(messages)
}
}, subscribeOptions)
} catch (e: VideoSDKException) {
Log.w("VideoSDK", "PubSub refused: ${e.code} ${e.name}")
}
}

Key points:

  • A catch (e: IllegalStateException) around a PubSub call no longer catches anything. publish, subscribe and unsubscribe now suspend until the server has answered, and throw VideoSDKException: 3035 / 3065 when the room is not joined or is reconnecting, 4087 / 4088 / 4089 when publish / subscribe / unsubscribe fails, and 3073 (ERROR_INVALID_PARAMETER) for a duplicate subscription or invalid options. 3035 and 4087 / 4088 / 4089 also reach onError; 3065 and 3073 are only thrown. Call them from a coroutine, and check every unsubscribe in onDestroy().
  • A subscribe that fails or is cancelled registers nothing. An unsubscribe that fails keeps the listener.
  • PubSubPublishOptions(persist = false, sendOnly = null) has no setters. Read the values back as persist and sendOnly.
  • PubSubMessage.timestamp is the server's ISO-8601 time string, or "" when the server sent none. If you sorted messages by the 2.2.0 Long, parse the string.
  • Subscriptions now carry over to the new room when you call switchTo(), and are kept through a reconnect: the SDK subscribes to the same topics again for you. In 2.2.0 switchTo() dropped every subscription, so apps subscribed again, usually in onMeetingJoined(switchRoomId). Subscribe once instead: a second subscribe with the same listener throws 3073, and with a new listener object your app handles every message twice.
  • History arrives only through onOldMessagesReceived(messages, info). info.isLast marks the last batch.
  • PubSubMessage.message, senderId, senderName, topic, id and payload are nullable.
  • PubSubSubscribeOptions(oldMessageLimit = null, realtimeOverflow = RealtimeOverflow.QUEUE, maxQueue = null, newMessageLimit = null) has no setters. Read the values back as properties. RealtimeOverflow moved to live.videosdk.rtc.android.model.RealtimeOverflow.

9. Streams are RTCStream, and WebRTC classes moved​

Stream is now RTCStream. Its kind is a MediaType (AUDIO, VIDEO, SHARE, SHARE_AUDIO) instead of a string, its track is non-null, and it has a new participantId. The WebRTC classes the SDK exposes moved from org.webrtc to videosdk.org.webrtc, and org.webrtc is no longer on your classpath. Change every import of VideoTrack, VideoFrame, MediaStreamTrack, RendererCommon and SurfaceViewRenderer.

Before

import org.webrtc.VideoTrack

class ParticipantViewListener(private val videoView: VideoView) : ParticipantEventListener() {
override fun onStreamEnabled(stream: Stream) {
if (stream.kind.equals("video", ignoreCase = true)) {
val track = stream.track as VideoTrack
videoView.addTrack(track)
}
}
}

After

import live.videosdk.rtc.android.RTCStream
import live.videosdk.rtc.android.MediaType
import videosdk.org.webrtc.VideoTrack

class ParticipantViewListener(private val videoView: VideoView) : ParticipantEventListener() {
override fun onStreamEnabled(stream: RTCStream) {
if (stream.kind == MediaType.VIDEO) {
val track = stream.track as VideoTrack
videoView.addTrack(track)
}
}
}

Key points:

  • participant.streams is a Map<String, RTCStream>.
  • The per-participant onStreamStateChanged(state, peerId, streamId) callback is gone. Set a listener on the stream instead: stream.setOnStreamStateChangedListener { state -> … }, for example in onStreamEnabled.
  • ParticipantEventListener.onE2eeStateChanged is now onE2EEStateChanged(state, stream: RTCStream).
  • onStreamPaused(kind, reason) and onStreamResumed(kind, reason) pass kind as a StreamKind? (AUDIO, VIDEO or SHARE) instead of a string. It is null for screen-share audio.
  • VideoView now extends videosdk.org.webrtc.SurfaceViewRenderer, and setScalingType takes videosdk.org.webrtc.RendererCommon.ScalingType. addTrack(null) is ignored.
  • CustomStreamTrack.track is null for screen-share tracks.
  • Tracks, your own camera's included, also behave differently when you call track methods such as addSink on them directly (see section 21).

10. Video frame processors are set on the room​

VideoSDK.applyVideoProcessor() and VideoSDK.removeVideoProcessor() are now room.setVideoProcessor() and room.removeVideoProcessor(). VideoFrameProcessor now receives and returns a videosdk.org.webrtc.VideoFrame, and the SDK releases both the frame it passes in and the frame you return, so stop releasing frames yourself.

Before

import org.webrtc.VideoFrame

val processor = object : VideoFrameProcessor {
override fun onFrameCaptured(videoFrame: VideoFrame): VideoFrame {
return applyFilter(videoFrame)
}
}
VideoSDK.applyVideoProcessor(processor)

After

import videosdk.org.webrtc.VideoFrame

val processor = VideoFrameProcessor { videoFrame: VideoFrame ->
// don't release either frame: the SDK releases both
applyFilter(videoFrame)
}
room.setVideoProcessor(processor)

Key points:

  • Set it after createRoom. It works before join(). The processor is removed when the room is left, so set it again for the next room.
  • The frame passed in is always an I420 frame. 2.2.0 passed the camera's texture frames, so a processor that casts the buffer to TextureBuffer must read it as an I420 buffer instead. If your processor throws, the original frame is sent unchanged.
  • Remove live.videosdk:videosdk-media-effects 0.0.2. It does not work with 3.0.0, and 3.0.0 has no built-in virtual background.
  • 3.0.0 has its own camera effects: auto-framing, low-light correction, stylized looks and color grades (see New in 3.0.0).

11. Device APIs changed​

Cameras and audio devices have new names, and audio routing moved to the room. getVideoDevices() is getCameras(), getAudioDevices() is getMics(), VideoDeviceInfo is CameraDeviceInfo, and AudioDeviceInfo is MicrophoneDeviceInfo. VideoSDK.setSelectedAudioDevice() is room.setAudioDevice(), and the device listener is set with room.setAudioDeviceChangeListener(). changeMic takes a deviceId string.

Before

VideoSDK.setAudioDeviceChangeListener { selectedAudioDevice, audioDevices ->
Log.d("VideoSDK", "Using ${selectedAudioDevice.label}, ${audioDevices.size} available")
}

val speaker = VideoSDK.getAudioDevices().first { it.label == "SPEAKER_PHONE" }
VideoSDK.setSelectedAudioDevice(speaker)

meeting.changeMic(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE, micTrack)

val backCamera = VideoSDK.getVideoDevices().first { it.facingMode == FacingMode.back }
meeting.changeWebcam(backCamera.deviceId)

After

room.setAudioDeviceChangeListener { selectedAudioDevice, audioDevices ->
Log.d("VideoSDK", "Using ${selectedAudioDevice?.label}, ${audioDevices.size} available")
}

val speaker = VideoSDK.getMics().first { it.deviceId == "SPEAKER_PHONE" }
room.setAudioDevice(speaker)

lifecycleScope.launch {
try {
room.changeMic(speaker.deviceId)
room.changeMic(micTrack)

val backCamera = VideoSDK.getCameras().first { it.facingMode == FacingMode.back }
room.changeCam(backCamera.deviceId)
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Device change failed: ${e.code} ${e.name}")
}
}

Key points:

  • VideoSDK.getCameras() and getMics() return a List.
  • AudioDeviceChangeEvent is in live.videosdk.rtc.android.listeners. Its selected device is nullable and the device list is a read-only List<MicrophoneDeviceInfo>.
  • room.setAudioDevice(device) throws VideoSDKException 3059 (ERROR_NO_MICROPHONE_DEVICE_FOUND), also reported on onError, for a device that is not in VideoSDK.getMics() right now.
  • The device listener stays registered after the room is left, until you replace it or pass null.
  • changeMic(deviceId) moves to another audio device, changeMic(track) publishes a different microphone track, and changeMic() with no argument moves to the next device. The combined changeMic(device, track) is gone.
  • VideoSDK.getDevices() returns a List<DeviceInfo>: the CameraDeviceInfo entries, then the MicrophoneDeviceInfo entries.
  • meeting.getMics() is gone. Use VideoSDK.getMics().
  • VideoSDK.getSelectedAudioDevice() and VideoSDK.getSelectedVideoDevice() now return MicrophoneDeviceInfo? and CameraDeviceInfo?.
  • AppRTCAudioManager now only holds the AudioDevice enum. Its methods and the AudioManagerEvents interface are gone.

12. Permission helpers changed​

checkPermissions is now one function with default arguments. A call that skips rationale or options, like checkPermissions(context, permissions, handler), no longer compiles: name the handler. The Permissions.Options setters became properties, and sendDontAskAgainToSettings is now sendBlockedToSettings.

Before

val permissions = listOf(Permission.audio, Permission.video)
val permissionHandler = object : PermissionHandler() {
override fun onGranted() {
Log.d("VideoSDK", "Granted")
}
}

VideoSDK.checkPermissions(activity, permissions, permissionHandler)

val options = Permissions.Options()
.setRationaleDialogTitle("Info")
.setSettingsDialogTitle("Warning")
.sendDontAskAgainToSettings(true)
VideoSDK.checkPermissions(activity, permissions, "Needed for the call", options, permissionHandler)

After

val permissions = listOf(Permission.audio, Permission.video)
val permissionHandler = object : PermissionHandler() {
override fun onGranted() {
Log.d("VideoSDK", "Granted")
}
}

VideoSDK.checkPermissions(activity, permissions, permissionHandler = permissionHandler)

val options = Permissions.Options().apply {
rationaleDialogTitle = "Info"
settingsDialogTitle = "Warning"
sendBlockedToSettings = true
}
VideoSDK.checkPermissions(activity, permissions, "Needed for the call", options, permissionHandler)

Key points:

  • The low-level Permissions.check is now one function that takes an Array<String> of permissions and a String rationale. The single-permission and string-resource forms are gone. Prefer VideoSDK.checkPermissions.

13. Getters became Kotlin properties​

You can no longer pick either syntax for a getter. A member that is now a property, such as room.localParticipant, can't be called as getLocalParticipant(). A member that is still a function, such as participant.getVideoStats(), can't be read as participant.videoStats.

Before

val meetingId = meeting.getMeetingId()
val localParticipant = meeting.getLocalParticipant()
val name = localParticipant.getDisplayName()

val multiStream = customTrack.isMultiStream
val facingMode: String = customTrack.facingMode
val cameraId = customTrack.videoDeviceId

val agentId = agent.agentId
val text = transcription.text

val options = PubSubSubscribeOptions()
options.oldMessageLimit = 50

After

val roomId = room.id
val localParticipant = room.localParticipant
val name = localParticipant.displayName

val multiStream = customTrack.multiStream
val facingMode: FacingMode = customTrack.facingMode
val cameraId = customTrack.cameraId

val agentId = agent.getAgentId()
val text = transcription.getText()

val options = PubSubSubscribeOptions(oldMessageLimit = 50)
Read as a propertyCall as a function
The getters of Room, Participant, RTCStream, CustomStreamTrack, PubSubMessage, PubSubSubscribeOptions, PubSubHistoryInfo, PubSubMessageDropInfo, DeviceInfo, CodecChangeInfo, TranscriptionSegment, VideoSDKError and LeaveReason, for example room.id, participant.displayName, stream.kind, reason.code, info.isLastparticipant.isAgent(), getVideoStats(), getAudioStats(), getShareStats(); room.getActivePresenterId(), getActiveSpeakerId(); AgentParticipant.getAgentId(), getAgentState(); the TranscriptionText getters

Key points:

  • CustomStreamTrack.isMultiStream(), getVideoDeviceId() and getFacingMode() are now multiStream, cameraId and facingMode, and facingMode is a FacingMode instead of a String.
  • meeting.isE2eeEnabled() is now room.e2eeEnabled.

14. Strings became typed values​

Several parameters and properties that took strings or a JSONObject now take enums or config classes.

Before

meeting.changeMode("SEND_AND_RECV")
participant.setQuality("high")
participant.pin("SHARE_AND_CAM")

val config = JSONObject()
val layout = JSONObject()
layout.put("type", "GRID")
layout.put("priority", "SPEAKER")
layout.put("gridSize", 4)
config.put("layout", layout)
config.put("theme", "DARK")
config.put("mode", "video-and-audio")
config.put("quality", "high")
config.put("orientation", "landscape")
meeting.startRecording(null, null, config, null)

After

lifecycleScope.launch {
try {
room.changeMode(Mode.SEND_AND_RECV)
participant.setQuality(VideoQuality.HIGH)
participant.pin(PinType.SHARE_AND_CAM)

room.startRecording(
config = RecordingConfig(
layout = ConfigLayout(type = ConfigLayoutGrid.GRID, priority = ConfigLayoutPriority.SPEAKER, gridSize = 4),
theme = ConfigTheme.DARK,
mode = ConfigMode.VIDEO_AND_AUDIO,
quality = ConfigQuality.HIGH,
orientation = ConfigOrientation.LANDSCAPE,
),
)
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "${e.code} ${e.name}")
}
}
2.2.03.0.0
MeetingMode, mode stringsMode (SEND_AND_RECV, SIGNALLING_ONLY, RECV_ONLY) in createRoom, changeMode and participant.mode
MeetingStateRoomState
Quality strings "high", "med", "low"VideoQuality.HIGH / MEDIUM / LOW in participant.setQuality, participant.videoQuality (was quality), participant.setScreenShareQuality and room.setWebcamQuality
Pin strings "SHARE_AND_CAM", "CAM", "SHARE"PinType. pin() and unpin() default to PinType.SHARE_AND_CAM. Read the current state from participant.pinState
stream.kind strings "audio", "video", "share", "shareAudio"MediaType.AUDIO / VIDEO / SHARE / SHARE_AUDIO
Kind strings in pauseAllStreams, resumeAllStreams, requestMediaRelay, onPausedAllStreams, onResumedAllStreams, onStreamPaused and onStreamResumedStreamKind.AUDIO / VIDEO / SHARE. In the calls, null means all three
Entry decision strings "allowed", "denied" in respondEntry and onEntryRespondedEntryDecision.ALLOWED / DENIED
Relay decision strings in onMediaRelayRequestResponseRelayDecision.ACCEPTED / REJECTED
TranscriptionText.getType(): StringTranscriptionType?: REALTIME while the text may still change, FULL_SENTENCE once it is final, null for a value the SDK does not know
TranscriptionText.getTimestamp(): IntLong, 0 when the server sent none
HLS status strings "HLS_STARTING" …HLSState
Recording and livestream status strings "RECORDING_STARTING", "LIVESTREAM_STARTING" …RecordingState, LiveStreamState
Recording, HLS and livestream JSONObject configsRecordingConfig, HLSConfig and LivestreamConfig, built from ConfigLayout, ConfigLayoutGrid, ConfigLayoutPriority, ConfigTheme, ConfigMode, ConfigQuality, ConfigOrientation and LivestreamRecordingConfig
TranscriptionConfig(webhookUrl, summary)TranscriptionConfig(webhookURL = null, summary = null, modelConfig = null)
Transcription state JSONObjectTranscriptionState

Key points:

  • Every config field is optional. A field you leave null takes the server's default.
  • startRecording(webhookUrl = null, awsDirPath = null, config = null, transcription = null), startHLS(config = null, transcription = null), startLivestream(outputs, config = null) and startTranscription(config = null) all have defaults now.
  • participant.setScreenShareQuality takes a VideoQuality as well.
  • room.recordingState, liveStreamState, hlsState, transcriptionState and translationState are non-null enums. Each one starts at its *_STOPPED value and goes back to it when the room is left.

15. Renamed APIs at a glance​

2.2.03.0.0
VideoSDK.config(token) + VideoSDK.initMeeting(context, meetingId, name, micEnabled, webcamEnabled, …, customTracks, …)VideoSDK.createRoom(roomId, token, participantName, micEnabled, camEnabled, …, customCameraTrack, customMicrophoneTrack, …)
Meeting, MeetingEventListenerRoom, RoomEventListener
StreamRTCStream
VideoSDK.createAudioTrack(encoderConfig, context)VideoSDK.createMicrophoneAudioTrack(encoderConfig, microphoneId, noiseConfig)
VideoSDK.getVideoDevices(), getAudioDevices()VideoSDK.getCameras(), getMics()
VideoDeviceInfo, AudioDeviceInfoCameraDeviceInfo, MicrophoneDeviceInfo
VideoSDK.setSelectedAudioDevice(device)room.setAudioDevice(device)
VideoSDK.setAudioDeviceChangeListener(listener), meeting.setAudioDeviceChangeListener(AudioManagerEvents)room.setAudioDeviceChangeListener(AudioDeviceChangeEvent)
VideoSDK.applyVideoProcessor(processor), removeVideoProcessor()room.setVideoProcessor(processor), removeVideoProcessor()
VideoSDK.createScreenShareVideoTrack(encoderConfig, data, context, listener)VideoSDK.createScreenShareVideoTrack(encoderConfig, data, multiStream, withAudio), which returns the track
VideoSDK.AudioDeviceChangeEventlive.videosdk.rtc.android.listeners.AudioDeviceChangeEvent
meeting.enableWebcam() / disableWebcam() / changeWebcam()room.enableCam() / disableCam() / changeCam()
participant.enableWebcam() / disableWebcam()participant.enableCam() / disableCam()
meeting.startHls() / stopHls()room.startHLS() / stopHLS()
meeting.enableAdaptiveSubscriptions() / disableAdaptiveSubscriptions()room.enableAdaptiveSubscription() / disableAdaptiveSubscription()
meeting.realtimeStore.set(key, value, callback) / get(key, callback)room.realtimeStore.setValue(key, value) / getValue(key)
meeting.enableScreenShare(track, enableAudio)room.enableScreenShare(track), with withAudio set on the track
meeting.changeMic(device, track)room.changeMic(deviceId) and room.changeMic(track)
meeting.getMics()VideoSDK.getMics()
meeting.getMeetingId(), getMeetingState(), isE2eeEnabled()room.id, roomState, e2eeEnabled
meeting.removeAllListeners(), participant.removeAllListeners()removeAllEventListeners()
participant.getQuality()participant.videoQuality
participant.captureImage(height, width, listener)participant.captureImage(width, height, flash)
ParticipantEventListener.onE2eeStateChangedonE2EEStateChanged
BaseKeyProvider.setKey(participantId, key)VideoSDK.setParticipantKey(participantId, key)
onOldMessagesReceived(messages)onOldMessagesReceived(messages, info: PubSubHistoryInfo)
Permissions.Options().sendDontAskAgainToSettings(value)Permissions.Options().sendBlockedToSettings = value
customTrack.isMultiStream() / getVideoDeviceId() / getFacingMode()customTrack.multiStream / cameraId / facingMode
org.webrtc.*videosdk.org.webrtc.*

Changes that compile but behave differently​

These changes compile without errors, so the compiler will not point you to them. Check the code each one affects.

16. Error codes were renumbered​

Code that compares error numbers keeps compiling and starts handling the wrong error. Some errors got a new number, some numbers now mean something else, and some errors are no longer reported on Android. Match on VideoSDKError constants instead of raw numbers.

Before

override fun onError(error: JSONObject) {
when (error.optInt("code")) {
3014 -> onReconnecting() // ERROR_MEETING_RECONNECTING in 2.2.0
3016 -> Unit // ERROR_ALREADY_IN_REQUESTED_STATE in 2.2.0
else -> Log.e("VideoSDK", error.optString("message"))
}
}

After

override fun onError(error: VideoSDKError) {
when (error) {
VideoSDKError.ERROR_ROOM_RECONNECTING -> onReconnecting()
VideoSDKError.ERROR_ALREADY_IN_REQUESTED_STATE -> Unit
else -> Log.e("VideoSDK", error.message)
}
}

New number

2.2.03.0.0
3001 ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED3035 ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED
3008 ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED3045 ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED
3009 ERROR_MULTISTREAM_NOT_SUPPORTED3046 ERROR_MULTISTREAM_NOT_SUPPORTED
3011 ERROR_BLUETOOTH_PERMISSION_DENIED3090 ERROR_BLUETOOTH_PERMISSION_DENIED
3014 ERROR_MEETING_RECONNECTING3065 ERROR_ROOM_RECONNECTING
3015 ERROR_OPERATION_ABORTED3056 ERROR_OPERATION_ABORTED
3016 ERROR_ALREADY_IN_REQUESTED_STATE3054 ERROR_ALREADY_IN_REQUESTED_STATE

Same number, new meaning

Code2.2.03.0.0
3011ERROR_BLUETOOTH_PERMISSION_DENIEDERROR_GET_VIDEO_MEDIA
3012ERROR_GET_SERVER_CONFIG_FAILEDERROR_GET_AUDIO_MEDIA
3013OPERATION_TIMEOUTERROR_GET_DISPLAY_MEDIA
3014ERROR_MEETING_RECONNECTINGERROR_GET_VIDEO_MEDIA_PERMISSION_DENIED
3015ERROR_OPERATION_ABORTEDERROR_GET_AUDIO_MEDIA_PERMISSION_DENIED
3016ERROR_ALREADY_IN_REQUESTED_STATEERROR_GET_DISPLAY_MEDIA_PERMISSION_DENIED

No longer reported on Android

2.2.0 errorWhat 3.0.0 reports instead
3002 ERROR_CAMERA_ACCESSA specific camera error: 3017, 3019, 3021, 3023, 3026 or 3027
3003 ERROR_MIC_ACCESS_DENIED3018 ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED
3004 ERROR_CAMERA_ACCESS_DENIED3017 ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED
3005 ERROR_CUSTOM_SCREEN_SHARE_TRACK_ENDED, 3006 ERROR_CUSTOM_SCREEN_SHARE_TRACK_DISPOSEDNothing. A screen-share track only carries the consent. If the consent can no longer be used when the share starts, enableScreenShare throws 3016 ERROR_GET_DISPLAY_MEDIA_PERMISSION_DENIED
3007 ERROR_CHANGE_WEBCAM3051 ERROR_NO_WEBCAM_DEVICE_FOUND or 3033 ERROR_CAMERA_ACCESS_UNAVAILABLE
3012 ERROR_GET_SERVER_CONFIG_FAILED (removed)join() throws (see section 18)
3013 OPERATION_TIMEOUT (removed)The PubSub calls throw 4087 / 4088 / 4089 while the PubSub connection is down
Error codes added in 3.0.0 that Android reports
CodeName
3017ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED
3018ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED
3019ERROR_CAMERA_PERMISSION_DENIED_BY_OS
3021ERROR_CAMERA_NOT_FOUND
3022ERROR_MICROPHONE_NOT_FOUND
3023ERROR_CAMERA_IN_USE
3024ERROR_MICROPHONE_IN_USE
3026ERROR_VIDEO_SOURCE_INITIATION_FAILED
3027ERROR_WEBCAM_TRACK_ENDED
3031ERROR_CUSTOM_VIDEO_TRACK_ENDED
3032ERROR_CUSTOM_AUDIO_TRACK_ENDED
3033ERROR_CAMERA_ACCESS_UNAVAILABLE
3034ERROR_MICROPHONE_ACCESS_UNAVAILABLE
3047ERROR_UNABLE_TO_JOIN_ROOM
3048ERROR_MEDIA_NOT_READY
3049ERROR_WEBCAM_PRODUCE_FAILED
3050ERROR_MEDIA_DEVICE_NOT_INITIALIZED
3051ERROR_NO_WEBCAM_DEVICE_FOUND
3052ERROR_NO_SUPPORTED_VIDEO_CODEC
3053ERROR_VIDEO_TRACK_CREATION_FAILED
3055ERROR_OPERATION_IN_PROGRESS
3057ERROR_END_ROOM_FAILED
3058ERROR_MICROPHONE_PRODUCE_FAILED
3059ERROR_NO_MICROPHONE_DEVICE_FOUND
3060ERROR_AUDIO_TRACK_CREATION_FAILED
3061ERROR_SCREENSHARE_PRODUCE_FAILED
3062ERROR_WEBCAM_NOT_ENABLED
3063ERROR_SET_WEBCAM_QUALITY_FAILED
3064ERROR_MEDIA_DEVICE_NOT_LOADED
3066ERROR_ACTION_NOT_SUPPORTED_IN_MODE
3068ERROR_ACTION_NOT_SUPPORTED
3069ERROR_CAPTURE_IMAGE_FAILED
3070ERROR_SEND_FAILED
3071ERROR_INVALID_CUSTOM_TRACK_CONFIG
3072ERROR_PAYLOAD_TOO_LARGE
3073ERROR_INVALID_PARAMETER
3074ERROR_FILE_UPLOAD_FAILED
3075ERROR_FILE_FETCH_FAILED
3076ERROR_WEBCAM_CLOSE_PRODUCER_FAILED
3077ERROR_MICROPHONE_CLOSE_PRODUCER_FAILED
3079ERROR_OPERATION_TIMED_OUT
4000UNKNOWN_ERROR
5000SOMETHING_WENT_WRONG
5015TRANSLATION_FAILED

Key points:

  • A thrown VideoSDKException carries the same code and name.
  • 4009 kept its number but its name is now spelled MAX_PARTCIPANT_REACHED. 2.2.0 used MAX_PARTICIPANT_REACHED. The new spelling is used in VideoSDKError, in the name of the error onError delivers and in VideoSDK.getErrorCodes(). Compare this one by code: error.code == 4009.
  • 4023 is spelled UNAUTHORIZED_PARTCIPANT_ID, as in 2.2.0. Compare it by code too: error.code == 4023.

17. Per-participant E2EE keys use setParticipantKey​

BaseKeyProvider.setKey() and KeyProvider.setKey() are gone. Set and rotate per-participant keys with VideoSDK.setParticipantKey instead. Keys you set are cleared when the room is left.

Before

override fun onParticipantJoined(participant: Participant) {
// the key from your server
baseKeyProvider.setKey(participant.id, keyFor(participant.id))
}

After

override fun onParticipantJoined(participant: Participant) {
// the key from your server
VideoSDK.setParticipantKey(participant.id, keyFor(participant.id))
}

Key points:

  • Call VideoSDK.setKeyProvider(provider) once before you call setParticipantKey. Without a provider, setParticipantKey throws 3073 (ERROR_INVALID_PARAMETER).
  • setParticipantKey also rotates a key during the call. Passing null drops that participant's key: this device then stops decrypting that participant until a new key is set. keyProvider.removeKey(participantId) on the installed provider does the same.
  • ParticipantEventListener.onE2EEStateChanged(state, stream) reports EncryptionSuccess on the local participant and DecryptionSuccess on the remote sender when encryption starts working on a stream, while a key provider is set. Decryption failures are not reported: a wrong or removed key shows only as the stream going dark.
  • Per-participant keys are cleared whenever the room is left. Set them again after each join.
  • room.e2eeEnabled is true only while media is actually being encrypted.
  • setKeyProvider no longer accepts null. To turn encryption off, call the new VideoSDK.removeKeyProvider().
  • getKeyProvider() was removed from KeyProvider and BaseKeyProvider. A subclass that overrides it must drop the override. BaseKeyProvider.key and keyMap are no longer public.

18. Calls that now throw instead of reporting on onError​

In 2.2.0 these calls reported a refusal on onError, or threw a different type of exception. In 3.0.0 they throw VideoSDKException, often without reporting on onError at all. An exception you don't catch crashes the app.

Before

fun onLeaveClicked() {
// a second tap only reported 3016 on onError
meeting.leave()
finish()
}

After

fun onLeaveClicked() {
lifecycleScope.launch {
try {
room.leave()
} catch (e: VideoSDKException) {
// 3035: never joined, 3054: already left
Log.w("VideoSDK", "Leave refused: ${e.code} ${e.name}")
}
finish()
}
}
Call2.2.03.0.0
join()Returned at once. A failed join reported 3012 on onError, then onMeetingStateChanged(FAILED)Throws 3055 if a join is already running, 3054 if this room or any other one is already joined, 3056 (ERROR_OPERATION_ABORTED) if leave() cancelled it, the server's refusal (an invalid or expired token, for example), or 3047 ERROR_UNABLE_TO_JOIN_ROOM. Leave a room before you join another one. Handle a failed join in the catch block. When the join itself fails, with 3047 or the server's code, onError and onRoomStateChanged(FAILED) still fire too, and the code on onError can differ
leave()Before joining or after leaving, reported 3001 / 3016 on onErrorThrows 3035 if the room was never joined and 3054 if it was already left, with nothing on onError. Called while a join is still running, it cancels that join
end()Outside a joined meeting, reported on onErrorThrows 3065 while reconnecting, 3035 when not in the room, 3057 when the server refuses and 3079 on timeout
muteMic, unmuteMic, enableCam, disableCam, enableScreenShare, disableScreenShareA repeated or overlapping call was not reportedThrow 3054 when already in the requested state and 3055 while another toggle of the same kind is running. For example, a second tap on a mute button while the first toggle is still running throws 3055
changeMode(mode)Switching to the current mode threw java.lang.ErrorThrows 3054 for the current mode and 3055 while another changeMode is running. Neither reaches onError. As in 2.2.0, a change to SEND_AND_RECV brings the mic and camera back as they were just before the mode change, and a participant who joined in a lower mode gets what createRoom asked for. Don't call unmuteMic() or enableCam() for a device that comes back on by itself: the call can throw 3054
switchTo, requestMediaRelay, stopMediaRelay with an empty room idIllegalArgumentExceptionVideoSDKException 3073 (ERROR_INVALID_PARAMETER)
pubSub.publish, subscribe, unsubscribeIllegalStateException / IllegalArgumentExceptionVideoSDKException (see section 8)
Track factoriesReturned null and reported 3003 / 3004Throw (see section 5)
captureImageThrew java.lang.Error when the camera was offThrows 3062 / 3069 (see section 7)
createRoomconfig accepted any value; initMeeting threw NullPointerException for a null meetingId or tokenThrows 3073 for an empty token or roomId, and IllegalStateException before VideoSDK.initialize()

Key points:

  • lifecycleScope is already cancelled when onDestroy() runs, and viewModelScope when onCleared() runs, so a leave() launched from either never runs. Leave the room before the screen is destroyed, for example from your leave button as above.
  • A leave() that is still running stops if its scope is cancelled, for example when the user closes the screen during the leave, and the room stays joined. To let it finish, call it as withContext(NonCancellable) { room.leave() }.

19. onError can arrive off the main thread​

In 2.2.0, onError always ran on the main thread. In 3.0.0 it can run on the thread that made a refused call, or on the camera or microphone thread for a device fault. Listener callbacks still arrive on the main thread, except the ones the SDK calls straight away on the calling thread: the onStreamEnabled replay in participant.addEventListener (see section 21), and the first call that setAudioDeviceChangeListener makes. Post to the main thread before you touch the UI: use runOnUiThread in an Activity, or viewModelScope.launch in a ViewModel.

Before

override fun onError(error: JSONObject) {
errorText.text = error.optString("message")
}

After

override fun onError(error: VideoSDKError) {
runOnUiThread { errorText.text = error.message }
}

20. Values the SDK does not recognise are dropped​

A stream with an unknown kind, a mode change to an unknown mode, and a transcription or HLS status outside the typed enums are no longer delivered to your listeners. 2.2.0 passed them on as strings or JSONObject payloads. Where a value is nullable, such as onEntryResponded's decision, the kind of onPausedAllStreams or TranscriptionText.getType(), an unknown value arrives as null instead.


21. Other behavior changes​

What2.2.03.0.0, and what to do
onRoomStateChanged(CONNECTED)onMeetingStateChanged(CONNECTED) fired as soon as the connection opened, before onMeetingJoined and before a waiting-room request was answeredFires right after onRoomJoined. Until then, including while a waiting-room request is pending, the last state onRoomStateChanged reported is CONNECTING, though room.roomState can already read CONNECTED
room.switchTo(roomId)Joined the new meeting in your current modeAlways joins the new room in SEND_AND_RECV mode, so an audience member in SIGNALLING_ONLY or RECV_ONLY arrives able to publish, like a host. For audience members, call leave(), then createRoom(mode = …) and join() for the new room instead of switchTo
Video tracksaddSink on stream.track and CustomStreamTrack.track drew into your renderer, your own camera includedaddSink does nothing on your own video tracks (stream.track of your VIDEO and SHARE streams, and CustomStreamTrack.track), or on some remote ones. Render video with VideoView.addTrack. To read your own camera frames, use room.setVideoProcessor
Remote audio trackssetVolume and setEnabled on the track changed playbackNeither call changes playback. To stop receiving a participant's audio, call stream.pause()
participant.addEventListenerOnly registered the listenerCalls onStreamEnabled straight away, on the calling thread, for every stream the participant already has. A loop over participant.streams that repeats what your onStreamEnabled does now handles each stream twice, so drop it. Keep a loop that is your only call to VideoView.addTrack
room.participantsA snapshot copyA live view that changes as people join and leave. Copy it before you iterate off the main thread
room.localParticipant.id before join()Set by initMeeting: your participantId, or a generated idEmpty until onRoomJoined unless you pass participantId to createRoom. Read it in onRoomJoined
room.changeCam() with no idchangeWebcam() toggled between the front and back camerasMoves to the next camera in VideoSDK.getCameras() order, wrapping around. To flip between front and back, pass the deviceId of the camera with the facingMode you want. A camera that is off stays off, and an unknown id reports and throws 3051
room.changeCam(…) and room.changeMic(deviceId) while the camera or mic is onSwitched devices in place, with no stream eventsTurns the camera or mic off and on again on the new device, so onStreamDisabled and then onStreamEnabled fire, for you and for everyone else. Don't read that pair as the user turning the device off
Exceptions thrown in your RoomEventListener, ParticipantEventListener, PubSubMessageListener or RealtimeStoreListener callbacksPropagated on the main threadCaught by the SDK, and the other listeners still run. RoomEventListener and RealtimeStoreListener exceptions are logged; ParticipantEventListener and PubSubMessageListener exceptions are dropped without a log, so catch and log them yourself
Camera presets H144P_W176P and H480P_W720PSent at their named sizesSent at 160x120 and 640x480
Screen-share presets h720p_15fps (the default) and h360p_30fpsSent at up to 400 and 200 kbpsSent at up to 1,000 and 400 kbps. Pick a lower preset, such as h720p_5fps or h480p_15fps, if upload bandwidth is tight
Screen-share notificationTitle, text and icon came from the notificationTitle, notificationContent and notificationIcon meta-data in your manifestThe meta-data is ignored. The notification always reads "VideoSDK is sharing your screen" with a system icon, and there is no API to change it
Swiping the app away during a screen shareDid not end the shareEnds the share
VideoSDK.disableTelemetry()The next initMeeting turned telemetry back onTelemetry stays off until the app process ends. enableTelemetry() does not restart it
onTranscriptionStateChangedPassed the server's payload as it arrived, including its idPasses only the TranscriptionState

Removed APIs​

22. Removed APIs and what to use instead​

Removed in 3.0.0Use instead
VideoSDK.config(token), VideoSDK.getToken()The token parameter of VideoSDK.createRoom
VideoSDK.setDebugMode(Boolean)VideoSDK.setLogLevel(LogLevel). setLogLevel() with no argument sets LogLevel.INFO, which is also the starting level
VideoSDK.maxLayer, VideoSDK.bitrateModeThe maxLayer and bitrateMode arguments of createCameraVideoTrack
MeetingMode.CONFERENCE, MeetingMode.VIEWER, the "CONFERENCE" / "VIEWER" stringsMode.SEND_AND_RECV, Mode.SIGNALLING_ONLY
TaskCompletionListener, RealtimeStoreCallbackThe return values of the suspend calls (see section 7)
onMeetingJoined(), onMeetingLeft(), onParticipantLeft(participant)The callbacks in section 3
onHlsStarted(), onHlsStopped()onHlsStateChanged with HLSState.HLS_STARTED / HLS_STOPPED
onMainParticipantChanged(participant)None
onExternalCallStarted()onExternalCallAnswered(callType)
RelayRequestListenerThe accept and reject functions passed to onMediaRelayRequestReceived
VideoSDK.CustomTrackListenerThe return value of createScreenShareVideoTrack (see section 6)
ParticipantEventListener.onStreamStateChanged(state, peerId, streamId)RTCStream.setOnStreamStateChangedListener
PubSubMessageListener.onOldMessagesReceived(messages)onOldMessagesReceived(messages, info)
VideoSDKError.values(), VideoSDKError.toJson()VideoSDK.getErrorCodes(), VideoSDKError.fromCode(code)
meeting.getMics()VideoSDK.getMics()
meeting.getActiveVideoCodec()The codec field of room.getAppliedVideoConfig(), and onCodecChanged for a switch to VP8
meeting.getAvailableVideoCodecs()None
participant.setMode(mode)room.changeMode(mode) for the local participant. Read any participant's mode from participant.mode
The AI Character feature: Character, CharacterConfig, CharacterEventListener, CharacterEventHandler, CharacterMessage, UserMessage, CharacterMode, CharacterState, onCharacterJoined and onCharacterLeftNone
Config and its getVideoProfiles(), getScreenShareProfiles(), getAudioProfiles()Pass a CustomCameraTrackConfig, CustomMicrophoneTrackConfig or screen-share profile name to the track factory
AppRTCAudioManager methods such as selectAudioDevice, getAudioDevices, getSelectedAudioDevice, setMode, start and stop, and AppRTCAudioManager.AudioManagerEventsVideoSDK.getMics, getSelectedAudioDevice and setMediaMode; room.setAudioDevice, changeMic and setAudioDeviceChangeListener
AppRTCAudioManager.AudioDevice.toInternal() / fromInternal()None
The VideoSDK fields audioManagerEvents, audioDeviceChangeEvent and audioRoutingManagerroom.setAudioDeviceChangeListener, room.setAudioDevice and room.changeMic
Singleton, including Singleton.isUserInMeetingroom.roomState, or Room.activeRoom() to find the room that is joined or joining
KeyProvider.getKeyProvider(), BaseKeyProvider.getKeyProvider(), KeyProvider.setKey(), BaseKeyProvider.setKey()VideoSDK.setParticipantKey (see section 17)
Meeting.getPeers(), Meeting.getConsumers()room.participants, participant.streams
Meeting.sendChatMessage()room.pubSub.publish(…) or room.send(…)
The hidden createCameraVideoTrack overload that took a CapturerObserverroom.setVideoProcessor(…) to see camera frames
Stream.getProducer(), Stream.getConsumer() and the Stream constructorsNone
CustomStreamTrack.getPeerConnectionUtils(), getVideoSource()None
CustomStreamTrack.VideoMode, CustomStreamTrack.getOptimizationMode() and the optimizationMode argument of createCameraVideoTrackNone. Remove the argument
Public constructors of Meeting, Participant, AgentParticipant, TranscriptionSegment and CodecChangeInfoUse the instances from VideoSDK.createRoom and the listener callbacks
Participant.e2eeListener(…)ParticipantEventListener.onE2EEStateChanged(state, stream)
AgentParticipant.handleAgentStateChanged, handleTranscriptionReceived, handleAgentMetricsParticipantEventListener.onAgentStateChanged, onAgentTranscriptionReceived, onAgentMetrics
VideoCodec.getMultiplier(), VideoCodec.getEffectiveBitrate(…)None
toJson() on TranscriptionConfig, SummaryConfig, PostTranscriptionConfig and TranscriptionText; toJSONObject() on LivestreamOutput and PubSubPublishOptions; PubSubSubscribeOptions.validate(); the static fromJSON / fromJSONObject helpers on TranscriptionSegment and PubSubMessageNone
SdkTracer's static methods and logLevel; the lib.tracing classes BackendLogOutput, ClientLogcatOutput, LogcatOutput, TelemetryOutput, TraceOutput, SpanContext, ComponentCategory, OutputTarget, RedactionConfig, RedactionMode, RoutedTracer and SpanScope (InspectorOutput remains); LogLevel.visibleCategories / shouldShowVideoSDK.setLogLevel, getLogLevel
lib.openTelemetry.VideoSDKLogs, VideoSDKTelemetryVideoSDK.enableTelemetry, disableTelemetry, isTelemetryEnabled
The lib.audio routing helpers AudioRouteApplier, LegacyRouteApplier, ModernRouteApplier, DeviceSelectionPolicy, DefaultDeviceSelectionPolicy, DeviceInventory, ProximityManager, WiredHeadsetManager, AudioCaptureController, AudioDevice and BluetoothManagerThe audio-device methods on VideoSDK and Room
ScreenShareService, PhoneCallBroadcastReceiver, PermissionsActivity, MeetingStateGuard, lib.ErrorsNone. The SDK still declares its components in its own manifest. Use VideoSDK.getErrorCodes() for the error list
Internal 2.2.0 classes, such as lib.RoomClient, lib.PeerConnectionUtils, lib.RoomOptions, lib.UrlFactory, lib.Utils, lib.SdkCapabilities, lib.SharedEglContext, lib.TemporaryFile, lib.AppRTCUtils, lib.AppRTCBluetoothManager, lib.AppRTCProximitySensor, lib.BitmapVideoFrameConversion, lib.VideoEncoderFactoryWrapper, lib.VideoEncoderWrapper, lib.socket.*, lib.pubsub.*, lib.lv.*, lib.model.* and VideoViewManagerNone
live.videosdk.rtc.android.lib.JsonUtilsorg.json directly: JSONObject.put, JSONObject(String), JSONArray(String)

New in 3.0.0​

FeatureAPI
Pre-call test of camera, microphone and networkVideoSDK.runPreCallTest(options, onStatsChange), a suspend function, and stopPreCallTest(); the types in live.videosdk.rtc.android.precall, such as PreCallTestResult, PreCallMediaReport and PreCallNetworkQuality. Cancelling the coroutine also stops the test. Failures are a PreCallTestException with a string code such as ERROR_PRECALL_INVALID_CONFIG. A test can't start after createRoom until that room is left (ERROR_PRECALL_AFTER_INIT)
Live translationroom.startTranslation, stopTranslation, changeTranslationLanguage; createRoom(translationLanguage = …, speakingLanguage = …); onTranslationStateChanged(state: TranslationState), onTranslationText, onTranslationLanguageChanged(participantId, language); TranslationState, TranslationText. changeTranslationLanguage throws 4044 (CHANGE_TRANSLATION_LANGUAGE_FAILED) when the server refuses, for example before translation has started. The new language applies once onTranslationLanguageChanged arrives
Messages over the data channelroom.send(message, reliable) for text or room.send(data, reliable) for a ByteArray, received in RoomEventListener.onDataReceived(DataMessage) with text, data and reliable. Up to 15 KiB, with no topics or history
Choose whose media you receivecreateRoom(autoConsume = false); participant.consumeMicStreams, consumeWebcamStreams, stopConsumingMicStreams, stopConsumingWebcamStreams; ParticipantEventListener.onMediaStatusChanged
Save upload bandwidthcreateRoom(dynaCast = true) for a VP8 simulcast camera; room.setWebcamQuality(quality: VideoQuality) caps what you send
Pick a remote screen share's qualityparticipant.setScreenShareQuality(quality)
Default camera settingscreateRoom(maxResolution = MaxResolution.sd, defaultCamera = FacingMode.back)
Camera effectsVideoSDK.setVideoEffects / room.setVideoEffects with a VideoEffectsConfig using Pipeline.GRAPH: auto-framing, low-light correction, stylized looks and color grades. Also getAppliedVideoEffectsConfig, VideoSDK.publishFramingSubject and VideoView.attachLocalPreview / detachLocalPreview
Flash for still capturesparticipant.captureImage(width, height, CaptureFlash.ON), VideoSDK.setScreenFlash(ScreenFlash.whiteOverlay(activity))
Microphone choice and audio processing per trackcreateMicrophoneAudioTrack(microphoneId = …, noiseConfig = NoiseConfig(…))
Track lifecycleCustomStreamTrack.stop(), isEnded, onEnded, plus frameRate, withAudio, microphoneId and noiseConfig
More presetsCamera: H90P_W160P, H120P_W160P, H180P_W240P, H180P_W320P, H360P_W480P, H540P_W960P, H1080P_W1920P. Screen share: h480p_15fps, h480p_30fps, h720p_30fps
Switch the camera to a track you createdroom.changeCam(customCameraTrack)
Share the screen straight from the consent Intentroom.enableScreenShare(data, enableAudio), a suspend function; enableAudio defaults to false
Room and media stateroom.isMicEnabled, isCamEnabled, isScreenSharing, getActivePresenterId(), getActiveSpeakerId(), recordingState, hlsState, liveStreamState, transcriptionState, translationState, getAppliedVideoConfig(); participant.pinState
Stream healthRTCStream.setOnStreamStateChangedListener with StreamState.FREEZE_DETECTED, FREEZE_RESOLVED, STUCK, ACTIVE and ENDED
More statisticsparticipant.getShareAudioStats(), participant.getTransportStats()
Rotate an E2EE key during a callVideoSDK.setParticipantKey(participantId, key)
Turn E2EE offVideoSDK.removeKeyProvider()
Remove one participant's E2EE keyKeyProvider.removeKey(participantId) (the same as setParticipantKey(participantId, null) on the installed provider)
Read the log levelVideoSDK.getLogLevel()
Errors as exceptionsVideoSDKException with code, name and error

Got a Question? Ask us on discord