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")
-
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
minSdkVersionfrom 21 to 24. -
ARM only. The SDK runs on
arm64-v8aandarmeabi-v7a. x86 / x86_64 emulator images are no longer supported: test on an ARM phone or anarm64-v8aemulator image. If your app ships native libraries for other ABIs, setndk { 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.webrtcclasses, 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-androidnow comes with the SDK. The examples below launch coroutines withlifecycleScope, which comes fromandroidx.lifecycle:lifecycle-runtime-ktx: add it if your app doesn't already have it. If your app keeps theRoomin aViewModel, useviewModelScopeinstead. -
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.orgclasses, which include the WebRTC classes. The WebRTC classes moved fromorg.webrtctovideosdk.org.webrtc(see section 9), so rules you wrote fororg.webrtc.**no longer match anything: replace them with these. Keep the-dontwarnline, 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:
createRoomtakes nocontext. CallVideoSDK.initialize(context)once first, for example inApplication.onCreate(). Without it,createRoomand the camera and microphone track factories throwIllegalStateException.- The token is a
createRoomparameter. An emptytokenorroomIdthrowsVideoSDKException3073(ERROR_INVALID_PARAMETER). A token the server rejects makesjoin()throw the server's code, such as4002(INVALID_TOKEN). - The parameters, in order:
roomId,token,participantName,micEnabled,camEnabled,participantId,mode,multiStream,customCameraTrack,customMicrophoneTrack,metaData,signalingBaseUrl,preferredProtocol,codecSwitchEnabled,enableBatching, then the newtranslationLanguage,speakingLanguage,maxResolution,defaultCamera,autoConsumeanddynaCast(see New in 3.0.0). Use named arguments. modeis aMode(Mode.SEND_AND_RECV,Mode.SIGNALLING_ONLY,Mode.RECV_ONLY), not a string.nullmeansSEND_AND_RECV. The old"CONFERENCE"and"VIEWER"strings are gone: useSEND_AND_RECVandSIGNALLING_ONLY.- The
customTracksmap is now two parameters:customCameraTrack(was the"video"entry) andcustomMicrophoneTrack(was the"mic"entry). multiStream = nullmeanstrue, andmaxResolution = nullmeansMaxResolution.hd.participantName = nulljoins 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'sdisplayNameis set onceonRoomJoinedfires.Room.activeRoom()replacesMeeting.activeMeeting(). It returns the room that is joined or joining, ornull.
The Room members that were renamed:
2.2.0 Meeting | 3.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:
VideoSDKExceptionis unchecked. One you don't catch crashes the app like any other uncaught exception.- It carries
codeandname, anderrorreturns the matchingVideoSDKError(see section 4). join()returns once the mic and camera you asked for increateRoomhave started or failed, soonRoomJoinedcan fire beforejoin()returns. Set up anythingonRoomJoinedreads, such as the field that holds the room, before you calljoin().- The PubSub calls
publish,subscribeandunsubscribearesuspendfunctions too (see section 8). - These are still plain functions:
realtimeStore.observeandstopObserving,room.setAudioDeviceChangeListener,room.setAudioDevice,room.setVideoProcessorandroom.removeVideoProcessor. - Called before the room is joined, or while it is reconnecting, most actions still report
3035/3065ononErrorand return without throwing.changeCam,changeMic,uploadBase64File,fetchBase64File, the PubSub calls andRTCStream.pause/resumereport the error and also throw it, except that the PubSub calls only throw3065.RealtimeStore.setValue/getValueandroom.sendthrow both codes, but only3035also reachesonError.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.isMicEnabledandroom.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 throws3054.
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 MeetingEventListener | 3.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 withnullon every join, with the new room id afterswitchTo(), and again after a reconnect that needed a fresh join. Don't use it for one-time setup.onEntryRequestedhands youallowanddeny: call one of them to answer.room.respondEntry(participantId, EntryDecision.ALLOWED)orEntryDecision.DENIEDstill works if you answer later.callTypein the external-call callbacks is aCallType:INCOMINGorOUTGOING. Android cannot tell a declined call from a missed one: both arrive asonExternalCallRejected.HLSUrlcarriesplaybackHlsUrlandlivestreamUrl. It is non-null when the server sent a URL, in practice withHLS_PLAYABLE. There is nodownstreamUrl.- Nullable:
onRoomJoined'sswitchRoomId,onPresenterChanged'spresenterId,onSpeakerChanged'sactiveSpeakerId,onPinStateChanged'spinnedBy,onHlsStateChanged'shlsUrl,onMediaRelayStopped'sreason, thedecisionofonEntryRespondedandonMediaRelayRequestResponse, thekindofonPausedAllStreams,onResumedAllStreams,onStreamPausedandonStreamResumed, and the selected device inAudioDeviceChangeEvent. Everything else is non-null. RealtimeStoreListener.onValueChanged(newValue: String?, updatedBy: Participant?). A cleared key arrives asnull, andupdatedByisnullwhen the writer has left or was never seen by this device.onRecordingStateChangedandonLivestreamStateChangedpass aRecordingState(RECORDING_STARTING…RECORDING_STOPPED) and aLiveStreamState(LIVESTREAM_STARTING…LIVESTREAM_STOPPED) instead of a string. Compare with the enum.onMediaRelayRequestResponsepasses aRelayDecision(ACCEPTEDorREJECTED) first and gained aroomId, so a 2.2.0 override no longer compiles: rewrite it in the new order.- Every callback of
RoomEventListenerandParticipantEventListenerhas an empty default, soobject : 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 awhen, never with===. - A
when (error)used as an expression needs anelsebranch, becauseVideoSDKErroris no longer an enum. VideoSDKError.values()andtoJson()are gone. UseVideoSDK.getErrorCodes()for the name-to-code map andVideoSDKError.fromCode(code)to look up one code.VideoSDKException.erroris a non-nullVideoSDKErrorcarrying 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.0 | 3.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"areCustomCameraTrackConfig.H720P_W1280P, and microphone strings such as"speech_standard"areCustomMicrophoneTrackConfig.SPEECH_STANDARD. The screen-share config is still a string. createCameraVideoTracktakescameraId(adeviceIdfromVideoSDK.getCameras()) instead of aVideoDeviceInfo, and the track reports it back asCustomStreamTrack.cameraId;maxLayeras anEncodingLayer(MAX_LAYER_2orMAX_LAYER_3), andcodecinstead ofvideoCodec. For a single layer, passmultiStream = false.CustomStreamTrack.videoCodecis nowcodec.- The factories suspend until the device is open, whatever thread you call them from. The camera throws
3023when another app holds it and3026for any other open failure. The microphone throws3024. - Other errors:
3017/3018whenCAMERA/RECORD_AUDIOis not granted,3021/3022when there is no camera / microphone, and3071for an unknown camera or microphone id. - Before
VideoSDK.initialize()has run, the factories throwIllegalStateException. VideoSDK.maxLayerandVideoSDK.bitrateModeare gone. PassmaxLayerandbitrateModetocreateCameraVideoTrackinstead.- The
optimizationModeargument andCustomStreamTrack.VideoModeare 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:
createScreenShareVideoTrackreturns at once. The track only carries the consent and the profile: capture and the foreground service start inenableScreenShare. 2.2.0 started the screen-share service at once, then started capturing and called the listener about two seconds later.- The track is
nullwhen the user declined the consent dialog, or whenencoderConfigis not a known profile. The second case also reports3071(ERROR_INVALID_CUSTOM_TRACK_CONFIG) ononError. encoderConfigdefaults to"h720p_15fps", andmultiStreamtofalse.room.enableScreenShare()with no track uses the consent from the lastcreateScreenShareVideoTrackcall, and throws3048(ERROR_MEDIA_NOT_READY) when there is none. Passing a camera track throws3071.- 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:
captureImagenow 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.uploadBase64Filereturns the uploaded file's URL, which is whatfetchBase64Filetakes.fetchBase64Filereturns the file's base64 content.captureImagereturns a base64 JPEG. It throws3062(ERROR_WEBCAM_NOT_ENABLED) when the camera is off, where 2.2.0 threwjava.lang.Error, and3069(ERROR_CAPTURE_IMAGE_FAILED) when the capture fails.getValueon a key that was never set, or was cleared, throws4046(FAILED_TO_GET_VALUE), also reported ononError. Catch it where an unset key is normal.setValue(key, null)clears the key, andsetValuereturns 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,subscribeandunsubscribenow suspend until the server has answered, and throwVideoSDKException:3035/3065when the room is not joined or is reconnecting,4087/4088/4089when publish / subscribe / unsubscribe fails, and3073(ERROR_INVALID_PARAMETER) for a duplicate subscription or invalid options.3035and4087/4088/4089also reachonError;3065and3073are only thrown. Call them from a coroutine, and check everyunsubscribeinonDestroy(). - 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 aspersistandsendOnly.PubSubMessage.timestampis the server's ISO-8601 time string, or""when the server sent none. If you sorted messages by the 2.2.0Long, 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.0switchTo()dropped every subscription, so apps subscribed again, usually inonMeetingJoined(switchRoomId). Subscribe once instead: a secondsubscribewith the same listener throws3073, and with a new listener object your app handles every message twice. - History arrives only through
onOldMessagesReceived(messages, info).info.isLastmarks the last batch. PubSubMessage.message,senderId,senderName,topic,idandpayloadare nullable.PubSubSubscribeOptions(oldMessageLimit = null, realtimeOverflow = RealtimeOverflow.QUEUE, maxQueue = null, newMessageLimit = null)has no setters. Read the values back as properties.RealtimeOverflowmoved tolive.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.streamsis aMap<String, RTCStream>.- The per-participant
onStreamStateChanged(state, peerId, streamId)callback is gone. Set a listener on the stream instead:stream.setOnStreamStateChangedListener { state -> … }, for example inonStreamEnabled. ParticipantEventListener.onE2eeStateChangedis nowonE2EEStateChanged(state, stream: RTCStream).onStreamPaused(kind, reason)andonStreamResumed(kind, reason)passkindas aStreamKind?(AUDIO,VIDEOorSHARE) instead of a string. It isnullfor screen-share audio.VideoViewnow extendsvideosdk.org.webrtc.SurfaceViewRenderer, andsetScalingTypetakesvideosdk.org.webrtc.RendererCommon.ScalingType.addTrack(null)is ignored.CustomStreamTrack.trackisnullfor screen-share tracks.- Tracks, your own camera's included, also behave differently when you call track methods such as
addSinkon 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 beforejoin(). 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
TextureBuffermust read it as an I420 buffer instead. If your processor throws, the original frame is sent unchanged. - Remove
live.videosdk:videosdk-media-effects0.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()andgetMics()return aList.AudioDeviceChangeEventis inlive.videosdk.rtc.android.listeners. Its selected device is nullable and the device list is a read-onlyList<MicrophoneDeviceInfo>.room.setAudioDevice(device)throwsVideoSDKException3059(ERROR_NO_MICROPHONE_DEVICE_FOUND), also reported ononError, for a device that is not inVideoSDK.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, andchangeMic()with no argument moves to the next device. The combinedchangeMic(device, track)is gone.VideoSDK.getDevices()returns aList<DeviceInfo>: theCameraDeviceInfoentries, then theMicrophoneDeviceInfoentries.meeting.getMics()is gone. UseVideoSDK.getMics().VideoSDK.getSelectedAudioDevice()andVideoSDK.getSelectedVideoDevice()now returnMicrophoneDeviceInfo?andCameraDeviceInfo?.AppRTCAudioManagernow only holds theAudioDeviceenum. Its methods and theAudioManagerEventsinterface 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.checkis now one function that takes anArray<String>of permissions and aStringrationale. The single-permission and string-resource forms are gone. PreferVideoSDK.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 property | Call 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.isLast | participant.isAgent(), getVideoStats(), getAudioStats(), getShareStats(); room.getActivePresenterId(), getActiveSpeakerId(); AgentParticipant.getAgentId(), getAgentState(); the TranscriptionText getters |
Key points:
CustomStreamTrack.isMultiStream(),getVideoDeviceId()andgetFacingMode()are nowmultiStream,cameraIdandfacingMode, andfacingModeis aFacingModeinstead of aString.meeting.isE2eeEnabled()is nowroom.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.0 | 3.0.0 |
|---|---|
MeetingMode, mode strings | Mode (SEND_AND_RECV, SIGNALLING_ONLY, RECV_ONLY) in createRoom, changeMode and participant.mode |
MeetingState | RoomState |
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 onStreamResumed | StreamKind.AUDIO / VIDEO / SHARE. In the calls, null means all three |
Entry decision strings "allowed", "denied" in respondEntry and onEntryResponded | EntryDecision.ALLOWED / DENIED |
Relay decision strings in onMediaRelayRequestResponse | RelayDecision.ACCEPTED / REJECTED |
TranscriptionText.getType(): String | TranscriptionType?: REALTIME while the text may still change, FULL_SENTENCE once it is final, null for a value the SDK does not know |
TranscriptionText.getTimestamp(): Int | Long, 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 configs | RecordingConfig, 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 JSONObject | TranscriptionState |
Key points:
- Every config field is optional. A field you leave
nulltakes the server's default. startRecording(webhookUrl = null, awsDirPath = null, config = null, transcription = null),startHLS(config = null, transcription = null),startLivestream(outputs, config = null)andstartTranscription(config = null)all have defaults now.participant.setScreenShareQualitytakes aVideoQualityas well.room.recordingState,liveStreamState,hlsState,transcriptionStateandtranslationStateare non-null enums. Each one starts at its*_STOPPEDvalue and goes back to it when the room is left.
15. Renamed APIs at a glance
| 2.2.0 | 3.0.0 |
|---|---|
VideoSDK.config(token) + VideoSDK.initMeeting(context, meetingId, name, micEnabled, webcamEnabled, …, customTracks, …) | VideoSDK.createRoom(roomId, token, participantName, micEnabled, camEnabled, …, customCameraTrack, customMicrophoneTrack, …) |
Meeting, MeetingEventListener | Room, RoomEventListener |
Stream | RTCStream |
VideoSDK.createAudioTrack(encoderConfig, context) | VideoSDK.createMicrophoneAudioTrack(encoderConfig, microphoneId, noiseConfig) |
VideoSDK.getVideoDevices(), getAudioDevices() | VideoSDK.getCameras(), getMics() |
VideoDeviceInfo, AudioDeviceInfo | CameraDeviceInfo, 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.AudioDeviceChangeEvent | live.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.onE2eeStateChanged | onE2EEStateChanged |
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.0 | 3.0.0 |
|---|---|
3001 ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED | 3035 ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED |
3008 ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED | 3045 ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED |
3009 ERROR_MULTISTREAM_NOT_SUPPORTED | 3046 ERROR_MULTISTREAM_NOT_SUPPORTED |
3011 ERROR_BLUETOOTH_PERMISSION_DENIED | 3090 ERROR_BLUETOOTH_PERMISSION_DENIED |
3014 ERROR_MEETING_RECONNECTING | 3065 ERROR_ROOM_RECONNECTING |
3015 ERROR_OPERATION_ABORTED | 3056 ERROR_OPERATION_ABORTED |
3016 ERROR_ALREADY_IN_REQUESTED_STATE | 3054 ERROR_ALREADY_IN_REQUESTED_STATE |
Same number, new meaning
| Code | 2.2.0 | 3.0.0 |
|---|---|---|
| 3011 | ERROR_BLUETOOTH_PERMISSION_DENIED | ERROR_GET_VIDEO_MEDIA |
| 3012 | ERROR_GET_SERVER_CONFIG_FAILED | ERROR_GET_AUDIO_MEDIA |
| 3013 | OPERATION_TIMEOUT | ERROR_GET_DISPLAY_MEDIA |
| 3014 | ERROR_MEETING_RECONNECTING | ERROR_GET_VIDEO_MEDIA_PERMISSION_DENIED |
| 3015 | ERROR_OPERATION_ABORTED | ERROR_GET_AUDIO_MEDIA_PERMISSION_DENIED |
| 3016 | ERROR_ALREADY_IN_REQUESTED_STATE | ERROR_GET_DISPLAY_MEDIA_PERMISSION_DENIED |
No longer reported on Android
| 2.2.0 error | What 3.0.0 reports instead |
|---|---|
3002 ERROR_CAMERA_ACCESS | A specific camera error: 3017, 3019, 3021, 3023, 3026 or 3027 |
3003 ERROR_MIC_ACCESS_DENIED | 3018 ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED |
3004 ERROR_CAMERA_ACCESS_DENIED | 3017 ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED |
3005 ERROR_CUSTOM_SCREEN_SHARE_TRACK_ENDED, 3006 ERROR_CUSTOM_SCREEN_SHARE_TRACK_DISPOSED | Nothing. 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_WEBCAM | 3051 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
| Code | Name |
|---|---|
| 3017 | ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED |
| 3018 | ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED |
| 3019 | ERROR_CAMERA_PERMISSION_DENIED_BY_OS |
| 3021 | ERROR_CAMERA_NOT_FOUND |
| 3022 | ERROR_MICROPHONE_NOT_FOUND |
| 3023 | ERROR_CAMERA_IN_USE |
| 3024 | ERROR_MICROPHONE_IN_USE |
| 3026 | ERROR_VIDEO_SOURCE_INITIATION_FAILED |
| 3027 | ERROR_WEBCAM_TRACK_ENDED |
| 3031 | ERROR_CUSTOM_VIDEO_TRACK_ENDED |
| 3032 | ERROR_CUSTOM_AUDIO_TRACK_ENDED |
| 3033 | ERROR_CAMERA_ACCESS_UNAVAILABLE |
| 3034 | ERROR_MICROPHONE_ACCESS_UNAVAILABLE |
| 3047 | ERROR_UNABLE_TO_JOIN_ROOM |
| 3048 | ERROR_MEDIA_NOT_READY |
| 3049 | ERROR_WEBCAM_PRODUCE_FAILED |
| 3050 | ERROR_MEDIA_DEVICE_NOT_INITIALIZED |
| 3051 | ERROR_NO_WEBCAM_DEVICE_FOUND |
| 3052 | ERROR_NO_SUPPORTED_VIDEO_CODEC |
| 3053 | ERROR_VIDEO_TRACK_CREATION_FAILED |
| 3055 | ERROR_OPERATION_IN_PROGRESS |
| 3057 | ERROR_END_ROOM_FAILED |
| 3058 | ERROR_MICROPHONE_PRODUCE_FAILED |
| 3059 | ERROR_NO_MICROPHONE_DEVICE_FOUND |
| 3060 | ERROR_AUDIO_TRACK_CREATION_FAILED |
| 3061 | ERROR_SCREENSHARE_PRODUCE_FAILED |
| 3062 | ERROR_WEBCAM_NOT_ENABLED |
| 3063 | ERROR_SET_WEBCAM_QUALITY_FAILED |
| 3064 | ERROR_MEDIA_DEVICE_NOT_LOADED |
| 3066 | ERROR_ACTION_NOT_SUPPORTED_IN_MODE |
| 3068 | ERROR_ACTION_NOT_SUPPORTED |
| 3069 | ERROR_CAPTURE_IMAGE_FAILED |
| 3070 | ERROR_SEND_FAILED |
| 3071 | ERROR_INVALID_CUSTOM_TRACK_CONFIG |
| 3072 | ERROR_PAYLOAD_TOO_LARGE |
| 3073 | ERROR_INVALID_PARAMETER |
| 3074 | ERROR_FILE_UPLOAD_FAILED |
| 3075 | ERROR_FILE_FETCH_FAILED |
| 3076 | ERROR_WEBCAM_CLOSE_PRODUCER_FAILED |
| 3077 | ERROR_MICROPHONE_CLOSE_PRODUCER_FAILED |
| 3079 | ERROR_OPERATION_TIMED_OUT |
| 4000 | UNKNOWN_ERROR |
| 5000 | SOMETHING_WENT_WRONG |
| 5015 | TRANSLATION_FAILED |
Key points:
- A thrown
VideoSDKExceptioncarries the samecodeandname. 4009kept its number but its name is now spelledMAX_PARTCIPANT_REACHED. 2.2.0 usedMAX_PARTICIPANT_REACHED. The new spelling is used inVideoSDKError, in thenameof the erroronErrordelivers and inVideoSDK.getErrorCodes(). Compare this one by code:error.code == 4009.4023is spelledUNAUTHORIZED_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 callsetParticipantKey. Without a provider,setParticipantKeythrows3073(ERROR_INVALID_PARAMETER). setParticipantKeyalso rotates a key during the call. Passingnulldrops 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)reportsEncryptionSuccesson the local participant andDecryptionSuccesson 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.e2eeEnabledistrueonly while media is actually being encrypted.setKeyProviderno longer acceptsnull. To turn encryption off, call the newVideoSDK.removeKeyProvider().getKeyProvider()was removed fromKeyProviderandBaseKeyProvider. A subclass that overrides it must drop the override.BaseKeyProvider.keyandkeyMapare 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()
}
}
| Call | 2.2.0 | 3.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 onError | Throws 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 onError | Throws 3065 while reconnecting, 3035 when not in the room, 3057 when the server refuses and 3079 on timeout |
muteMic, unmuteMic, enableCam, disableCam, enableScreenShare, disableScreenShare | A repeated or overlapping call was not reported | Throw 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.Error | Throws 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 id | IllegalArgumentException | VideoSDKException 3073 (ERROR_INVALID_PARAMETER) |
pubSub.publish, subscribe, unsubscribe | IllegalStateException / IllegalArgumentException | VideoSDKException (see section 8) |
| Track factories | Returned null and reported 3003 / 3004 | Throw (see section 5) |
captureImage | Threw java.lang.Error when the camera was off | Throws 3062 / 3069 (see section 7) |
createRoom | config accepted any value; initMeeting threw NullPointerException for a null meetingId or token | Throws 3073 for an empty token or roomId, and IllegalStateException before VideoSDK.initialize() |
Key points:
lifecycleScopeis already cancelled whenonDestroy()runs, andviewModelScopewhenonCleared()runs, so aleave()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 aswithContext(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
| What | 2.2.0 | 3.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 answered | Fires 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 mode | Always 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 tracks | addSink on stream.track and CustomStreamTrack.track drew into your renderer, your own camera included | addSink 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 tracks | setVolume and setEnabled on the track changed playback | Neither call changes playback. To stop receiving a participant's audio, call stream.pause() |
participant.addEventListener | Only registered the listener | Calls 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.participants | A snapshot copy | A 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 id | Empty until onRoomJoined unless you pass participantId to createRoom. Read it in onRoomJoined |
room.changeCam() with no id | changeWebcam() toggled between the front and back cameras | Moves 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 on | Switched devices in place, with no stream events | Turns 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 callbacks | Propagated on the main thread | Caught 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_W720P | Sent at their named sizes | Sent at 160x120 and 640x480 |
Screen-share presets h720p_15fps (the default) and h360p_30fps | Sent at up to 400 and 200 kbps | Sent 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 notification | Title, text and icon came from the notificationTitle, notificationContent and notificationIcon meta-data in your manifest | The 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 share | Did not end the share | Ends the share |
VideoSDK.disableTelemetry() | The next initMeeting turned telemetry back on | Telemetry stays off until the app process ends. enableTelemetry() does not restart it |
onTranscriptionStateChanged | Passed the server's payload as it arrived, including its id | Passes only the TranscriptionState |
Removed APIs
22. Removed APIs and what to use instead
| Removed in 3.0.0 | Use 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.bitrateMode | The maxLayer and bitrateMode arguments of createCameraVideoTrack |
MeetingMode.CONFERENCE, MeetingMode.VIEWER, the "CONFERENCE" / "VIEWER" strings | Mode.SEND_AND_RECV, Mode.SIGNALLING_ONLY |
TaskCompletionListener, RealtimeStoreCallback | The 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) |
RelayRequestListener | The accept and reject functions passed to onMediaRelayRequestReceived |
VideoSDK.CustomTrackListener | The 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 onCharacterLeft | None |
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.AudioManagerEvents | VideoSDK.getMics, getSelectedAudioDevice and setMediaMode; room.setAudioDevice, changeMic and setAudioDeviceChangeListener |
AppRTCAudioManager.AudioDevice.toInternal() / fromInternal() | None |
The VideoSDK fields audioManagerEvents, audioDeviceChangeEvent and audioRoutingManager | room.setAudioDeviceChangeListener, room.setAudioDevice and room.changeMic |
Singleton, including Singleton.isUserInMeeting | room.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 CapturerObserver | room.setVideoProcessor(…) to see camera frames |
Stream.getProducer(), Stream.getConsumer() and the Stream constructors | None |
CustomStreamTrack.getPeerConnectionUtils(), getVideoSource() | None |
CustomStreamTrack.VideoMode, CustomStreamTrack.getOptimizationMode() and the optimizationMode argument of createCameraVideoTrack | None. Remove the argument |
Public constructors of Meeting, Participant, AgentParticipant, TranscriptionSegment and CodecChangeInfo | Use the instances from VideoSDK.createRoom and the listener callbacks |
Participant.e2eeListener(…) | ParticipantEventListener.onE2EEStateChanged(state, stream) |
AgentParticipant.handleAgentStateChanged, handleTranscriptionReceived, handleAgentMetrics | ParticipantEventListener.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 PubSubMessage | None |
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 / shouldShow | VideoSDK.setLogLevel, getLogLevel |
lib.openTelemetry.VideoSDKLogs, VideoSDKTelemetry | VideoSDK.enableTelemetry, disableTelemetry, isTelemetryEnabled |
The lib.audio routing helpers AudioRouteApplier, LegacyRouteApplier, ModernRouteApplier, DeviceSelectionPolicy, DefaultDeviceSelectionPolicy, DeviceInventory, ProximityManager, WiredHeadsetManager, AudioCaptureController, AudioDevice and BluetoothManager | The audio-device methods on VideoSDK and Room |
ScreenShareService, PhoneCallBroadcastReceiver, PermissionsActivity, MeetingStateGuard, lib.Errors | None. 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 VideoViewManager | None |
live.videosdk.rtc.android.lib.JsonUtils | org.json directly: JSONObject.put, JSONObject(String), JSONArray(String) |
New in 3.0.0
| Feature | API |
|---|---|
| Pre-call test of camera, microphone and network | VideoSDK.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 translation | room.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 channel | room.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 receive | createRoom(autoConsume = false); participant.consumeMicStreams, consumeWebcamStreams, stopConsumingMicStreams, stopConsumingWebcamStreams; ParticipantEventListener.onMediaStatusChanged |
| Save upload bandwidth | createRoom(dynaCast = true) for a VP8 simulcast camera; room.setWebcamQuality(quality: VideoQuality) caps what you send |
| Pick a remote screen share's quality | participant.setScreenShareQuality(quality) |
| Default camera settings | createRoom(maxResolution = MaxResolution.sd, defaultCamera = FacingMode.back) |
| Camera effects | VideoSDK.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 captures | participant.captureImage(width, height, CaptureFlash.ON), VideoSDK.setScreenFlash(ScreenFlash.whiteOverlay(activity)) |
| Microphone choice and audio processing per track | createMicrophoneAudioTrack(microphoneId = …, noiseConfig = NoiseConfig(…)) |
| Track lifecycle | CustomStreamTrack.stop(), isEnded, onEnded, plus frameRate, withAudio, microphoneId and noiseConfig |
| More presets | Camera: 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 created | room.changeCam(customCameraTrack) |
Share the screen straight from the consent Intent | room.enableScreenShare(data, enableAudio), a suspend function; enableAudio defaults to false |
| Room and media state | room.isMicEnabled, isCamEnabled, isScreenSharing, getActivePresenterId(), getActiveSpeakerId(), recordingState, hlsState, liveStreamState, transcriptionState, translationState, getAppliedVideoConfig(); participant.pinState |
| Stream health | RTCStream.setOnStreamStateChangedListener with StreamState.FREEZE_DETECTED, FREEZE_RESOLVED, STUCK, ACTIVE and ENDED |
| More statistics | participant.getShareAudioStats(), participant.getTransportStats() |
| Rotate an E2EE key during a call | VideoSDK.setParticipantKey(participantId, key) |
| Turn E2EE off | VideoSDK.removeKeyProvider() |
| Remove one participant's E2EE key | KeyProvider.removeKey(participantId) (the same as setParticipantKey(participantId, null) on the installed provider) |
| Read the log level | VideoSDK.getLogLevel() |
| Errors as exceptions | VideoSDKException with code, name and error |
Got a Question? Ask us on discord

