stale-microphone-76591
12/30/2025, 8:39 AMpublishData(). Below is the function we’re using:
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.tall-airline-16756
12/30/2025, 8:39 AMtall-airline-16756
12/30/2025, 8:39 AMpublishData() 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 | DeepWikistale-microphone-76591
12/30/2025, 8:43 AMsendText() (before/after RoomEvent.Connected), and
adtertall-airline-16756
12/30/2025, 8:43 AMsendText() 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:
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:
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.stale-microphone-76591
12/30/2025, 9:55 AMtall-airline-16756
12/30/2025, 9:55 AMonDataReceived 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):
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):
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.stale-microphone-76591
12/30/2025, 10:16 AMctx.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.
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.tall-airline-16756
12/30/2025, 10:16 AMpublishData() 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.stale-microphone-76591
12/30/2025, 10:19 AMtall-airline-16756
12/30/2025, 10:19 AMregister_text_stream_handler("chat", ...). Call this after you see the room is connected.
1) Single-message send (simple):
// ensure room?.state == Room.State.CONNECTED
room?.localParticipant?.sendText(
text = "Hello from Android",
topic = "chat"
)
2) Streaming multiple chunks (if you want incremental chunks):
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