Skip to main content
Version: 3.x.x

PubSub - Android

PubSub (publish-subscribe) sends messages on named topics. A participant publishes a message to a topic, and every participant subscribed to that topic receives it.

Here is how the flow looks.

pubsub

tip

For frequent updates that don't need a topic or history, such as cursor positions, the Data Channel is lighter than PubSub.

pubSub​

Use room.pubSub, the room's PubSub object, to publish and subscribe to any topic.

publish()​

publish() sends a message to a topic. It takes:

  • topic: the topic to publish to.
  • message: the message, as a String.
  • options (optional): a PubSubPublishOptions. Pass its two options to the constructor as named arguments, for example PubSubPublishOptions(persist = true, sendOnly = arrayOf("participantId")). Both are optional.
    • persist: true keeps the message for the rest of the session. Participants who subscribe later receive it on onOldMessagesReceived(), and it is available as a .csv file in the VideoSDK Session Dashboard after the session. Defaults to false, which delivers the message only to the participants subscribed right now.
    • sendOnly: the participantIds of the participants who should receive the message, as an Array<String>. null (the default) or an empty array sends it to every subscriber.
  • payload (optional): extra data to send with the message, as a JSONObject. It comes after options, so pass it last or as a named argument.
private fun sendMessage() {
// publish message
val options = PubSubPublishOptions(persist = true)
lifecycleScope.launch {
try {
room!!.pubSub.publish("CHAT", "Hello Everyone!", options)
} catch (e: VideoSDKException) {
// Room is not joined, is reconnecting, or the message could not be sent
Log.d("VideoSDK", "publish failed: ${e.code} ${e.message}")
}
}
}

subscribe()​

subscribe() starts delivering a topic's messages to a listener. It takes:

  • topic: the topic to subscribe to.

  • listener: a PubSubMessageListener with the following callbacks. Each callback has an empty default body, so override only the ones you need.

    • onMessageReceived(message) — Called for every live message on the topic, in publish order, including the messages you publish yourself.

      • Use this when: You want to handle each message individually, such as appending it to a chat, triggering a notification, or updating message-specific state.
      • Don't use this when: The topic is high-throughput / large-scale (busy chats, live reactions, cursor positions). Firing once per message causes excessive UI updates — use onBatchReceived() instead.
    • onBatchReceived(messages) — Called once per live batch with all messages of that batch. These are the same messages delivered through onMessageReceived(), only grouped — use one path or the other, not both.

      • Use this when: Handling messages in bulk is cheaper, such as one UI update or one database transaction per batch instead of one per message.
      • Note: This callback fires only when batching is turned on for the room on the server side, so your app must work with and without it. Every message of a batch also arrives on onMessageReceived(). Handle each message in one callback or the other, never both, or you will count it twice.
    • onOldMessagesReceived(messages, info) — Called with the topic's persisted history in batches, once per batch with a partial list. Only messages published with persist enabled are delivered here. Append each batch to what you already hold rather than replacing it. info.isLast is true on the final batch. A topic with no persisted history never fires this callback.

      • Use this when: Loading chat history or restoring previously sent messages when a participant joins the room, or hiding a loader once the full history is rendered.
    • onMessageDrop(info) — Called when some live messages on the topic were not delivered to this participant. This can happen:

      • because this participant fell further behind than maxQueue allows under QUEUE, or
      • because newMessageLimit is set, which caps how many live messages this participant accepts every 500 ms. Messages over the cap are skipped.

      info.droppedCount is the number of messages that were not delivered. One callback can cover many messages. A loss is reported only once delivery resumes, so messages skipped at the very end of a burst are never reported. This callback never fires when realtimeOverflow is DROP.

      • Use this when: Notifying users that some realtime messages were missed, monitoring the effects of newMessageLimit, or logging dropped messages for debugging and observability.
  • options (optional): a PubSubSubscribeOptions. PubSubSubscribeOptions has 4 options, which you pass to its constructor as named arguments, for example PubSubSubscribeOptions(oldMessageLimit = 100, maxQueue = 70). All are optional. They are read-only properties afterwards.

    • oldMessageLimit: how many persisted messages are delivered when you subscribe. Pass null for all history, 0 for none, or N for the last N messages. Defaults to null.
    • realtimeOverflow: what happens when live messages arrive faster than they can be delivered. It is a RealtimeOverflow. QUEUE buffers them and delivers them paced, dropping the oldest on overflow; DROP keeps only the newest messages of each delivery window. Defaults to RealtimeOverflow.QUEUE.
    • maxQueue: how far this participant may fall behind under QUEUE, counted in delivery batches. Messages beyond it are reported on onMessageDrop(). It must be 1 or more and is only valid with QUEUE: combining it with DROP makes subscribe() throw a VideoSDKException with code 3073. If you don't provide any value, the server decides. The first participant to subscribe to a topic sets it for everyone.
    • newMessageLimit: the most live messages this participant accepts every 500 ms. null (the default) or 0 means unlimited.

A PubSubMessage has the following properties. They are nullable, so read them with a fallback, for example message.message ?: "".

  • id: the message's unique ID. Type String?.
  • message: the message that was sent. Type String?.
  • senderId: the participantId of the sender. Compare it with room.localParticipant.id to recognise your own messages. Type String?.
  • senderName: the displayName of the sender. Type String?.
  • timestamp: when the server accepted the message, as an ISO-8601 UTC string such as 2026-10-05T09:30:00.000Z. It is "" when the server sent none. Type String.
  • topic: the topic the message was published to. Type String?.
  • payload: the JSONObject sent with the message, or null if there was none. Type JSONObject?.

Call subscribe() once the room is joined. The example below runs in an activity opened after the room is joined. If the same activity joins the room, subscribe from onRoomJoined() instead, as shown in Raise Hand using PubSub. Called before the join completes, subscribe() throws 3035.

val pubSubMessageListener = object : PubSubMessageListener {
override fun onMessageReceived(message: PubSubMessage) {
Log.d("#message", "onMessageReceived: ${message.message}")
}

override fun onOldMessagesReceived(messages: List<PubSubMessage>, info: PubSubHistoryInfo) {
Log.d("#message", "onOldMessagesReceived: ${messages.size} messages, isLast: ${info.isLast}")
}

override fun onBatchReceived(messages: List<PubSubMessage>) {
Log.d("#message", "onBatchReceived: ${messages.size} messages")
}

override fun onMessageDrop(info: PubSubMessageDropInfo) {
Log.d("#message", "onMessageDrop: dropped ${info.droppedCount} messages")
}
}

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

// Subscribe for 'CHAT' topic
lifecycleScope.launch {
try {
room!!.pubSub.subscribe("CHAT", pubSubMessageListener)
} catch (e: VideoSDKException) {
// Room is not joined, is reconnecting, or the subscription failed
Log.d("VideoSDK", "subscribe failed: ${e.code} ${e.message}")
}
}
}

unsubscribe()​

unsubscribe() stops delivering a topic's messages to a listener. It takes:

  • topic: the topic to unsubscribe from.
  • listener: the PubSubMessageListener you passed to subscribe().
fun unsubscribe() {
// Unsubscribe 'CHAT' topic
lifecycleScope.launch {
try {
room!!.pubSub.unsubscribe("CHAT", pubSubMessageListener)
} catch (e: VideoSDKException) {
// Room is not joined (or has been left), is reconnecting, or the server call failed
Log.d("VideoSDK", "unsubscribe failed: ${e.code} ${e.message}")
}
}
}
caution

All three methods are suspend functions, so call them from a coroutine. They return once the server has answered, and throw a VideoSDKException on failure. They also require a joined room. See Errors for what each method throws.

Errors​

PubSub reports failures by throwing from the method (try / catch). Every failure except 3065 and 3073 also reaches RoomEventListener.onError(VideoSDKError). All three methods throw VideoSDKException. Use its code to tell the cases apart.

publish() throws:

  • 3035 (ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED) — The room has not been joined yet (or has already been left). Also fires onError.
  • 3065 (ERROR_ROOM_RECONNECTING) — The room is reconnecting. Not reported on onError. The message is not sent later, so publish it again once the room is CONNECTED.
  • 4087 (PUBSUB_PUBLISH_FAILED) — The message could not be sent, for example because the server rejected it, or the connection dropped before the server acknowledged it. Also fires onError. Publish it again once the room is CONNECTED.

subscribe() throws:

  • 3073 (ERROR_INVALID_PARAMETER) — "You already subscribed to this topic with same listener." One listener instance may hold at most one subscription per topic. Call unsubscribe() before re-subscribing, or create a second listener instance.
  • 3073 (ERROR_INVALID_PARAMETER) — oldMessageLimit is negative. It must be null (all history) or >= 0.
  • 3073 (ERROR_INVALID_PARAMETER) — maxQueue is less than 1.
  • 3073 (ERROR_INVALID_PARAMETER) — maxQueue was set while realtimeOverflow is DROP. It is only valid with QUEUE.
  • 3073 (ERROR_INVALID_PARAMETER) — newMessageLimit is negative. It must be null or >= 0.
  • 3035 (ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED) — The room has not been joined yet (or has already been left). Also fires onError.
  • 3065 (ERROR_ROOM_RECONNECTING) — The room is reconnecting. Not reported on onError.
  • 4088 (PUBSUB_SUBSCRIBE_FAILED) — The subscription failed, for example because the server rejected it or the connection is down. Also fires onError. Subscribe again once the room is CONNECTED.

When subscribe() throws, or its coroutine is cancelled, nothing is registered. The 3073 errors are not reported on onError. Options are validated before the room state is checked, so an invalid configuration fails immediately.

unsubscribe() throws:

The topic itself is unsubscribed only when you remove its last listener, and only that call can throw. Removing any other listener, or one that is not subscribed to the topic, never throws.

  • 3035 (ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED) — The room has not been joined yet (or has already been left). Also fires onError.
  • 3065 (ERROR_ROOM_RECONNECTING) — The room is reconnecting. Not reported on onError.
  • 4089 (PUBSUB_UNSUBSCRIBE_FAILED) — The server call failed, for example because the connection is down. Also fires onError.

When unsubscribe() throws, the listener stays registered.

Applications of PubSub​

Some features you can build with PubSub:

  1. Chat: You can utilise this to develop various Chat features, such as Private Chat and Group Chat. You can follow our chat integration guide here.
  2. Raise Hand: You can allow attendees to raise their hands at any time in the room, informing everyone else that someone has done so.
  3. Layout Switching: You can change the room's layout for every participant at once, such as from Grid layout to Spotlight or from Grid Layout to Sidebar.
  4. Poll: You may make polls, let users respond to them, and display the results at the end of a poll.
  5. Question Answer Session: You can also design interactive functionality that is question-and-answer based.

Downloading PubSub Messages​

All the messages from PubSub published with persist : true can be downloaded as a .csv file. This file will be available in the VideoSDK dashboard as well as through the Sessions API.

API Reference​

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

Got a Question? Ask us on discord