Migration Guide
This guide covers what you need to change when upgrading the Android SDK from v1.x.x to v2.0.0. Most of the work is in PubSub, plus one rename in the onError payload.
Update the dependency first:
implementation 'live.videosdk:rtc-android-sdk:2.0.1'
1. PubSub methods now throw
publish(), subscribe() and unsubscribe() require a joined meeting. In v1.x.x an ill-timed call failed silently; in v2.0.0 it throws IllegalStateException.
The most common place this bites is an unsubscribe() in onDestroy(), which frequently runs after the meeting has already ended.
Key points:
- Subscribe from
onMeetingJoined()rather than fromonCreate()of the activity that joins the meeting — atonCreate()time the meeting is not connected yet.
2. Persisted messages now arrive in batches
In v1.x.x, onOldMessagesReceived(List<PubSubMessage>) was called once with the topic's complete history. In v2.0.0 history is replayed in server-paced batches, so the same callback is called once per batch with a partial list.
Before
- Kotlin
- Java
// Backing state for your chat UI
val chatMessages = mutableStateListOf<PubSubMessage>()
override fun onOldMessagesReceived(messages: List<PubSubMessage>) {
// Called once with the complete history — safe to replace
chatMessages.clear()
chatMessages.addAll(messages)
}
// Backing list for your chat UI
private final List<PubSubMessage> chatMessages = new ArrayList<>();
@Override
public void onOldMessagesReceived(List<PubSubMessage> messages) {
// Called once with the complete history — safe to replace
chatMessages.clear();
chatMessages.addAll(messages);
}
After
- Kotlin
- Java
// Backing state for your chat UI
val chatMessages = mutableStateListOf<PubSubMessage>()
override fun onOldMessagesReceived(messages: List<PubSubMessage>, info: PubSubHistoryInfo) {
// Called once per batch — append, don't replace
chatMessages.addAll(messages)
if (info.isLast) {
// Every persisted message for this topic has now been delivered
isHistoryLoaded = true
}
}
// Backing list for your chat UI
private final List<PubSubMessage> chatMessages = new ArrayList<>();
@Override
public void onOldMessagesReceived(List<PubSubMessage> messages, PubSubHistoryInfo info) {
// Called once per batch — append, don't replace
chatMessages.addAll(messages);
if (info.isLast()) {
// Every persisted message for this topic has now been delivered
isHistoryLoaded = true;
}
}
Key points:
- If you keep the one-argument
onOldMessagesReceived(messages), it still receives every batch — but you must append rather than replace, and it carries no end-of-replay signal. - A topic with no persisted history fires no history callback at all. In v1.x.x it fired once with an empty list, so if you used that as a "replay finished" signal, move to
info.isLast(). - Use
oldMessageLimit(see section 4) to cap how much history you fetch, or pass0to skip it entirely.
3. New PubSub listener callbacks, and all of them are optional
In v1.x.x, PubSubMessageListener declared two abstract methods, so every implementation had to provide both. In v2.0.0 the interface has five methods and all of them are default — implement only the ones you need.
- Kotlin
- Java
val pubSubMessageListener = object : PubSubMessageListener {
// Existing — fires once per real-time message
override fun onMessageReceived(message: PubSubMessage) { }
// Existing — now called once per history batch
override fun onOldMessagesReceived(messages: List<PubSubMessage>) { }
// New — history batches, with an end-of-replay signal
override fun onOldMessagesReceived(messages: List<PubSubMessage>, info: PubSubHistoryInfo) { }
// New — the same real-time messages, grouped into a batch
override fun onBatchReceived(messages: List<PubSubMessage>) { }
// New — real-time messages shed because the buffer overflowed
override fun onMessageDrop(info: PubSubMessageDropInfo) { }
}
PubSubMessageListener pubSubMessageListener = new PubSubMessageListener() {
// Existing — fires once per real-time message
@Override
public void onMessageReceived(PubSubMessage message) { }
// Existing — now called once per history batch
@Override
public void onOldMessagesReceived(List<PubSubMessage> messages) { }
// New — history batches, with an end-of-replay signal
@Override
public void onOldMessagesReceived(List<PubSubMessage> messages, PubSubHistoryInfo info) { }
// New — the same real-time messages, grouped into a batch
@Override
public void onBatchReceived(List<PubSubMessage> messages) { }
// New — real-time messages shed because the buffer overflowed
@Override
public void onMessageDrop(PubSubMessageDropInfo info) { }
};
Key points:
- Your existing v1.x.x listener still compiles — the two methods it implements are now optional rather than required.
- On a busy topic, prefer
onBatchReceived()overonMessageReceived()— one UI update per batch instead of one per message. They deliver the same messages, so use one or the other, not both. - Learn more: PubSubMessageListener
4. New subscribe() options
subscribe() gained a third parameter of type PubSubSubscribeOptions, used to control how much history you fetch and what happens when messages arrive faster than your app can drain them. The two-parameter overload is unchanged, so existing calls keep working.
- Kotlin
- Java
val options = PubSubSubscribeOptions()
options.oldMessageLimit = 50 // fetch the 50 most recent persisted messages
options.realtimeOverflow = PubSubSubscribeOptions.RealtimeOverflow.QUEUE
options.maxQueue = 100 // only valid with QUEUE
meeting!!.pubSub.subscribe("CHAT", pubSubMessageListener, options)
PubSubSubscribeOptions options = new PubSubSubscribeOptions();
options.setOldMessageLimit(50); // fetch the 50 most recent persisted messages
options.setRealtimeOverflow(PubSubSubscribeOptions.RealtimeOverflow.QUEUE);
options.setMaxQueue(100); // only valid with QUEUE
meeting.pubSub.subscribe("CHAT", pubSubMessageListener, options);
Key points:
oldMessageLimit—null(default) fetches all history,0fetches none,Nfetches the most recentN.realtimeOverflow—QUEUE(default) buffers messages so you receive them once you catch up, dropping the oldest on overflow and reporting the count throughonMessageDrop();DROPsheds the excess silently. UseQUEUEfor chat where every message matters,DROPfor high-frequency, low-value streams such as reactions or cursor positions.maxQueue— the buffer ceiling underQUEUE. Combining it withDROPthrowsIllegalArgumentException.newMessageLimit— caps how many live messages you accept per 500 ms delivery window.nullor0means unlimited.- Invalid options are rejected synchronously, before any network work, so a bad configuration fails immediately rather than at runtime.
- Learn more: PubSubSubscribeOptions
5. One subscription per listener, per topic
A listener instance may hold at most one subscription to a given topic. Subscribing the same listener to the same topic twice now throws IllegalArgumentException instead of silently registering it again.
What you should do:
- Call
unsubscribe()before re-subscribing the same listener — for example when an activity or fragment is re-created. - If you need to handle the same topic in two places, create a second listener instance and subscribe it separately.
6. New error codes
v2.0.0 adds seven error codes, delivered through MeetingEventListener.onError(JSONObject).
| Name | Code | Meaning |
|---|---|---|
ERROR_GET_SERVER_CONFIG_FAILED | 3012 | Unable to fetch server configuration during meeting join. |
OPERATION_TIMEOUT | 3013 | PubSub operation timed out due to a disconnected or closed connection. |
FAILED_TO_OBSERVE_VALUE | 4085 | Failed to start observing the realtime store value. |
FAILED_TO_STOP_OBSERVING_VALUE | 4086 | Failed to stop observing the realtime store value. |
PUBSUB_PUBLISH_FAILED | 4087 | The message could not be delivered. |
PUBSUB_SUBSCRIBE_FAILED | 4088 | The subscription was rejected; the listener is unregistered again. |
PUBSUB_UNSUBSCRIBE_FAILED | 4089 | The unsubscribe was rejected; the listener is removed locally either way. |
Key points:
- These are reported asynchronously through
onError, unlike theIllegalStateExceptionin section 1, which is thrown at the call site. - Learn more: Meeting Error Codes
7. The onError payload key type is now name
The payload delivered to MeetingEventListener.onError(JSONObject) still carries code and message unchanged, but the constant-name key was renamed from type to name.
There is no compatibility alias. Code reading error.getString("type") will no longer find a value.
Before
- Kotlin
- Java
override fun onError(error: JSONObject) {
val code = error.optInt("code")
val type = error.optString("type")
Log.e("#error", "Error $code ($type)")
}
@Override
public void onError(JSONObject error) {
int code = error.optInt("code");
String type = error.optString("type");
Log.e("#error", "Error " + code + " (" + type + ")");
}
After
- Kotlin
- Java
override fun onError(error: JSONObject) {
val code = error.optInt("code")
val name = error.optString("name")
Log.e("#error", "Error $code ($name): ${error.optString("message")}")
}
@Override
public void onError(JSONObject error) {
int code = error.optInt("code");
String name = error.optString("name");
Log.e("#error", "Error " + code + " (" + name + "): " + error.optString("message"));
}
Key points:
codeandmessageare unchanged — only the constant-name key moved.nameholds the enum constant, for example"PUBSUB_SUBSCRIBE_FAILED", which is useful when correlating Android logs with your other platforms.VideoSDK.getErrorCodes()still returns the name → code map.- Learn more: Error Events
API Reference
Got a Question? Ask us on discord

