Context We are using the LiveKit Android SDK for r...
# ask-ai
s
Context We are using the LiveKit Android SDK for real-time chat-style messaging via
publishData()
. Below is the function we’re using:
Copy code
suspend fun sendText(text: String, topic: String = "lk.chat"): Boolean {
    Log.d("LiveKitManager", "canPublishData=" + room?.localParticipant?.permissions?.canPublishData)
    return try {
        val payload = text.toByteArray(Charsets.UTF_8)
        val res = room?.localParticipant?.publishData(
            data = payload,
            reliability = DataPublishReliability.RELIABLE,
            topic = "chat"
        )
        Log.d("LiveKitManager","The res is $res")
        res?.onFailure { Log.e("LiveKitManager", "publishData failed", it) }
        res?.isSuccess == true
    } catch (e: Exception) {
        false
    }
}
Observations 1.
canPublishData
is true for the local participant.
publishData()
returns a success result (no exception thrown). However, the data is not received on the remote participant side. The
topic
parameter passed to the function (
"lk.chat"
) seems to be ignored, and the SDK examples/documentation often show
"lk.chat"
as a default topic. 1. Changing the topic value (e.g.,
"chat"
,
"custom"
) does not appear to affect delivery behavior. Questions / Clarifications Needed 1. Is the topic effectively hardcoded internally (e.g.,
"lk.chat"
), or is it fully user-defined?
2. Do remote participants need to explicitly listen/filter by topic to receive data? 3. Is there any required setup on the receiver side beyond
onDataReceived
callbacks?
4. Are there known constraints on using
publishData()
for chat-style messaging at scale?
5. Is
DataPublishReliability.RELIABLE
sufficient for guaranteed delivery, or are there additional requirements (e.g., participant state, room configuration, permissions timing)?
6. Are there differences in behavior between
publishData()
and
streamText()
that would make one more suitable for chat messaging?
Environment • Platform: Android • LiveKit Android SDK (latest stable) • Room is successfully connected • Permissions verified on server side Any guidance on whether this is expected behavior, a configuration issue, or a limitation of the SDK would be very helpful. If there’s a recommended pattern for implementing chat using LiveKit data channels on Android, I’d appreciate pointers to that as well. Thanks in advance for your help.
t
Thanks for reaching out, Apratim. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’ll address each of your questions and point out the most likely causes for data not being received. Acknowledgement • You’re asking about topic handling, receiver setup, delivery guarantees, scale, and whether
publishData()
is the right approach for chat on Android. I’ll answer each and give a recommended pattern. Answers 1) Is the topic hardcoded or user-defined? • Topics are user-defined.
lk.chat
is just an example used in docs — the SDK does not hardcode that string. 2) Do remote participants need to explicitly listen/filter by topic? • The SDK sends a topic value, but the Android SDK’s received-data structure does not reliably expose topic-based routing like some JS examples. Best practice: include a message type/topic inside your payload so receivers can parse and filter themselves. 3) Any receiver setup beyond
onDataReceived
? • Ensure the Room/PeerConnection and DataChannel are fully established before sending. Only send after you’ve received the
RoomEvent.Connected
(room connected) or equivalent connected state. Other than that, implement
onDataReceived
on the Room and parse the payload (and any embedded topic/type) there. 4) Known constraints at scale? • Data uses WebRTC DataChannels (protobuf DataPacket).
RELIABLE
gives ordered delivery, but DataChannels aren’t designed for extremely high-frequency or very large payloads — high message rates can cause congestion/delay. For chat volumes typical of messaging apps it’s fine; for massive scale or high-throughput, consider batching, rate-limiting, or another dedicated messaging backend. 5) Is
DataPublishReliability.RELIABLE
sufficient for guaranteed delivery? •
RELIABLE
provides ordered, best-effort delivery over the established WebRTC connection, but it depends on: the PeerConnection/DataChannel being established, valid permissions on the participant’s token, and that the connection remains healthy. If you send before the connection is ready or after permissions expire, delivery can fail. 6) Differences between
publishData()
and
streamText()
? • The Android SDK does not document a
streamText()
method for general chat. For normal participant-to-participant chat use
publishData()
. Most likely cause of your missing messages • Timing: sending before the DataChannel/room connection is fully established is the most common reason messages appear “sent” (no exception) but are not received. Confirm you only call
sendText()
after
RoomEvent.Connected
(or equivalent connected state) and that the remote participant is also connected. Recommendations / pattern for Android chat 1. Wait for the room to be connected (handle
RoomEvent.Connected
) before sending. 2. Use
DataPublishReliability.RELIABLE
. 3. Put a small JSON or protobuf wrapper in the payload that includes
type/topic
,
senderId
,
timestamp
,
body
. This avoids relying on SDK-exposed topic routing. Example payload: {"type":"chat","sender":"alice","ts":...,"text":"Hello"} encoded as UTF-8. 4. On receiver: implement
onDataReceived
, decode payload, check
type
, then display/process. 5. For higher message volumes: rate-limit, batch, or use a messaging service if you hit congestion. If you’d like, tell me: • Where in your connection flow you call
sendText()
(before/after
RoomEvent.Connected
), and • The code you use to register
onDataReceived
on the receiver. I can then point out exact spots to change or add a small timing check you can use before publishing. Sources: Client SDKs | livekit/livekit_composite | DeepWiki | client-sdk-android | livekit/livekit_composite | DeepWiki | livekit/livekit_composite | DeepWiki | Simple Content Filter | LiveKit docs | client-sdk-swift | livekit/livekit_composite | DeepWiki
s
• Where in your connection flow you call
sendText()
(before/after
RoomEvent.Connected
), and adter
t
Thanks — since you call
sendText()
after
RoomEvent.Connected
, that rules out the most common timing issue. Let’s narrow the cause with a few quick checks and a small receiver verification. Checks to run (very quick) • Confirm remote participant is also in Connected state (has received
RoomEvent.Connected
) before expecting data. • Verify your
onDataReceived
handler is registered on the Room (or participant) and is active when messages are sent. Share the handler code if unsure. • Log incoming payload raw bytes and length on receiver to confirm nothing is delivered but ignored. • Make sure topics/payload: since Android may not expose topic reliably, include a simple JSON wrapper in the payload (e.g., {"type":"chat","text":"hi","sender":"alice"}) so receiver parsing doesn’t depend on SDK topic routing. Minimal diagnostic code suggestions 1) Ensure you register the receiver early (example pattern): • Register
onDataReceived
as soon as you create/observe the Room object so it’s active before any messages arrive. 2) When receiving, decode and log everything:
Copy code
room.onDataReceived = { participant, payload, topic ->
  Log.d("Recv","from=${participant.sid} topic=$topic len=${payload.size}")
  val text = payload.toString(Charsets.UTF_8)
  Log.d("Recv","payload=$text")
}
(If your SDK callback signature differs, log whatever fields are provided.) 3) On sender, add a quick check and log:
Copy code
if (room?.state != Room.State.CONNECTED) {
  Log.w("Send","room not connected: ${room?.state}")
}
Log.d("Send","publishing topic=chat len=${payload.size}")
If those logs show the sender publishing and receiver seeing nothing: • Paste the sender publish logs and receiver logs here (timestamps help). • Also paste the exact
onDataReceived
registration code you use. If receiver logs show messages but you ignore them in parsing, share the payload bytes printed above and I’ll help decode/parsing logic.
s
Tried all this, but did not work
t
Thanks — I know this is frustrating. Since you’ve already tried the basic checks, let’s narrow the problem with a few targeted diagnostics so I can pinpoint what’s failing. Please provide these 4 short items (copy/paste is fine): 1) Your exact
onDataReceived
registration code (receiver side). 2) Sender logs around publish (include the Log.d lines you added — timestamps helpful). 3) Receiver logs around incoming data (the Log.d from
onDataReceived
if any). 4) The Room and participant states on both sides when sending (values of
room?.state
and the remote participant’s
state
or
isConnected
if available). If you prefer a quick check first, run this minimal end-to-end test on both devices and paste the logs: Receiver (register immediately after obtaining
room
):
Copy code
room.onDataReceived = { participant, payload, topic ->
  Log.d("LKTEST_RECV", "from=${participant.sid} topic=$topic len=${payload.size}")
  Log.d("LKTEST_RECV", "payload=${payload.toString(Charsets.UTF_8)}")
}
Sender (run after you see RoomEvent.Connected locally and remote participant connected):
Copy code
val payload = "test_message_from_sender".toByteArray(Charsets.UTF_8)
Log.d("LKTEST_SEND","roomState=${room?.state} publishing len=${payload.size}")
val res = room?.localParticipant?.publishData(payload, DataPublishReliability.RELIABLE, "chat")
Log.d("LKTEST_SEND","publish res=$res")
res?.onFailure { Log.e("LKTEST_SEND","publish failed", it) }
Send once, then paste both LKTEST logs. If there are no receiving logs, I’ll ask one more quick runtime check to confirm whether the DataChannel is open at the WebRTC level.
s
1.
Copy code
ctx.room.register_text_stream_handler("chat", handle_text_stream)

async def handle_text_stream(
    reader: TextStreamReader,
    participant_identity: str,
):
    <http://logger.info|logger.info>(f"📩 Text stream started from {participant_identity}")

    full_message = ""

    try:
        async for chunk in reader:
            full_message += chunk

        # Detect transcription vs chat
        is_transcription = (
            reader.info.attributes.get("lk.transcribed_track_id") is not None
        )

        if is_transcription:
            <http://logger.info|logger.info>(
                f"🗣️ Transcription from {participant_identity}: {full_message}"
            )
        else:
            <http://logger.info|logger.info>(
                f"💬 Chat message from {participant_identity}: {full_message}"
            )

        # OPTIONAL: forward text into the agent
        await session.generate_reply(user_input=full_message)

    except Exception as e:
        logger.error("❌ Error reading text stream", exc_info=e)
3.
Copy code
suspend fun sendText(text: String, topic: String = "lk.chat"): Boolean {
    Log.d("LiveKitManager", "canPublishData=" + room?.localParticipant?.permissions?.canPublishData)
    return try {
        val payload = text.toByteArray(Charsets.UTF_8)
        val res = room?.localParticipant?.publishData(
            data = payload,
            reliability = DataPublishReliability.RELIABLE,
            topic = "chat"
        )
        Log.d("LiveKitManager","The res is $res")
        res?.onFailure { Log.e("LiveKitManager", "publishData failed", it) }
        return res?.isSuccess == true
    } catch (e: Exception) {
        false
    }
} --> LOGS : 2025-12-30 15:32:12.270 11763-11763 LKTEST_SEND             com.ai.meanttobe.debug               D  roomState=CONNECTED publishing len=5
2025-12-30 15:32:12.284 11763-11763 LKTEST_SEND             com.ai.meanttobe.debug               D  publish res=Success(kotlin.Unit)
3. No logs since message was not received 4.
t
Thanks — this is helpful. I can see you’re publishing raw data via
publishData()
while the server-side code is using
register_text_stream_handler("chat", ...)
(the text stream handler). Those are different APIs: publishing to the DataChannel with
publishData()
does not invoke the text stream handler on the server. What to do next (two options) • If you want to use the server’s `register_text_stream_handler`: publish a text stream from Android instead of raw
publishData()
. Use the text-stream API (
publishText
/ text stream methods) so the server-side
TextStreamReader
receives chunks. (The Android SDK example you used earlier is for DataChannels; text streams are a different transport/handler.) • If you want to keep using `publishData()`: implement a server-side
onDataReceived
handler (or equivalent) that reads the DataChannel payload directly and processes chat messages. Also embed a small JSON/type field in the payload (e.g., {"type":"chat","text":"hi"}) since topic may not be surfaced the same way on the server text-stream handler. Quick concrete guidance • You are currently doing: Android ->
publishData()
(DataChannel). Server expects: text stream ->
register_text_stream_handler("chat", ...)
. That mismatch is why the server logs never show a message. • Pick one path and align both sides: either switch Android to publish a text stream, or switch server to read DataChannel messages. If you want, tell me which you prefer and I’ll paste the minimal Android code to publish a text stream (client-side) or the minimal server-side code to receive DataChannel data so it matches your existing sender.
s
give minimal android code for the same
t
Below is minimal Android Kotlin code to publish a text stream message that matches your server's
register_text_stream_handler("chat", ...)
. Call this after you see the room is connected. 1) Single-message send (simple):
Copy code
// ensure room?.state == Room.State.CONNECTED
room?.localParticipant?.sendText(
    text = "Hello from Android",
    topic = "chat"
)
2) Streaming multiple chunks (if you want incremental chunks):
Copy code
val stream = room?.localParticipant?.createTextStream(topic = "chat")
stream?.use { writer ->
    writer.write("Hello ").await()
    writer.write("from ").await()
    writer.write("Android").await()
    writer.close().await()
}
Notes • Call these only after RoomEvent.Connected and ensure the server-side handler listens on topic "chat". • If you get compile errors, share the SDK version and I’ll adapt the snippet. Sources: Text and transcriptions | LiveKit docs