Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/data-received-participant-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"client-sdk-android": patch
---

Expose `participantIdentity` on `RoomEvent.DataReceived`. The event's `participant` is nullable and is null whenever the sender is not in the room — data sent through the Server API, or by a participant that has already left. In that case the receiver had no way to tell who sent the data, because the engine dropped the identity before building the event: `onUserPacket` was handed only the inner `UserPacket`, while the authoritative identity lives on the enclosing `DataPacket`.

The internal engine listener now forwards that identity, and it is reported on every `DataReceived` event whether or not the sender resolves to a `RemoteParticipant`.

This matches the JavaScript SDK (livekit/client-sdk-js#2092).
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,19 @@ sealed class RoomEvent(val room: Room) : Event() {
/**
* Received data published by another participant
*
* [participant] is null when the sender is not in the room, such as data published through
* the Server API or by a participant that has already left. Use [participantIdentity] to
* attribute the data in that case.
*
* ```
* room.events.collect { event ->
* if (event is RoomEvent.DataReceived) {
* val sender = event.participant?.identity?.value ?: event.participantIdentity
* println("$sender sent ${event.data.size} bytes")
* }
* }
* ```
*
* @param data the published data
* @param participant the participant if available
*/
Expand All @@ -256,8 +269,27 @@ sealed class RoomEvent(val room: Room) : Event() {
val participant: RemoteParticipant?,
val topic: String?,
val encryptionType: LivekitModels.Encryption.Type,
/**
* Identity of the sender. Reported even when [participant] is null because the
* sender is not (or is no longer) a participant in this room.
*/
val participantIdentity: String = "",
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
) :
RoomEvent(room)
RoomEvent(room) {
/**
* Retains the pre-[participantIdentity] JVM constructor signature, so modules compiled
* against an earlier SDK keep linking. A Kotlin default argument alone would not: it
* emits the full-arity constructor plus a synthetic one, and the old five-argument
* signature would disappear.
*/
constructor(
room: Room,
data: ByteArray,
participant: RemoteParticipant?,
topic: String?,
encryptionType: LivekitModels.Encryption.Type,
) : this(room, data, participant, topic, encryptionType, "")
}

/**
* The connection quality for a participant has changed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1186,7 +1186,7 @@ internal constructor(
fun onRoomUpdate(update: LivekitModels.Room)
fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>)
fun onSpeakersChanged(speakers: List<LivekitModels.SpeakerInfo>)
fun onUserPacket(packet: LivekitModels.UserPacket, kind: LivekitModels.DataPacket.Kind, encryptionType: LivekitModels.Encryption.Type)
fun onUserPacket(packet: LivekitModels.UserPacket, kind: LivekitModels.DataPacket.Kind, encryptionType: LivekitModels.Encryption.Type, participantIdentity: String)
fun onStreamStateUpdate(streamStates: List<LivekitRtc.StreamStateInfo>)
fun onSubscribedQualityUpdate(subscribedQualityUpdate: LivekitRtc.SubscribedQualityUpdate)
fun onRequestSubscribedCodecRefresh() {}
Expand Down Expand Up @@ -1550,7 +1550,7 @@ internal constructor(
}

LivekitModels.DataPacket.ValueCase.USER -> {
listener?.onUserPacket(dp.user, dp.kind, encryptionType)
listener?.onUserPacket(dp.user, dp.kind, encryptionType, dp.participantIdentity)
}

LivekitModels.DataPacket.ValueCase.SIP_DTMF -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1391,7 +1391,7 @@ constructor(
/**
* @suppress
*/
override fun onUserPacket(packet: LivekitModels.UserPacket, kind: LivekitModels.DataPacket.Kind, encryptionType: LivekitModels.Encryption.Type) {
override fun onUserPacket(packet: LivekitModels.UserPacket, kind: LivekitModels.DataPacket.Kind, encryptionType: LivekitModels.Encryption.Type, participantIdentity: String) {
val participant = getParticipantBySid(packet.participantSid) as? RemoteParticipant
val data = packet.payload.toByteArray()
val topic = if (packet.hasTopic()) {
Expand All @@ -1400,7 +1400,7 @@ constructor(
null
}

val event = RoomEvent.DataReceived(this, data, participant, topic, encryptionType)
val event = RoomEvent.DataReceived(this, data, participant, topic, encryptionType, participantIdentity)
eventBus.postEvent(event, coroutineScope)
participant?.onDataReceived(event)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2026 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,6 +28,7 @@ import livekit.LivekitModels.DataPacket
import livekit.LivekitModels.UserPacket
import livekit.org.webrtc.DataChannel
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import java.nio.ByteBuffer

Expand Down Expand Up @@ -62,4 +63,37 @@ class RoomDataMockE2ETest : MockE2ETest() {
val event = events[0] as RoomEvent.DataReceived
assertEquals("hello", event.data.decodeToString())
}

@Test
fun dataReceivedReportsSenderIdentityWithoutParticipant() = runTest {
connect()
val subPeerConnection = component.rtcEngine().getSubscriberPeerConnection() as MockPeerConnection
val subDataChannel = MockDataChannel(RTCEngine.RELIABLE_DATA_CHANNEL_LABEL)
subPeerConnection.observer?.onDataChannel(subDataChannel)

val collector = EventCollector(room.events, coroutineRule.scope)
// Data sent by the Server API, or by a participant that has already left, has no
// RemoteParticipant in the room to attach to.
val dataPacket = with(DataPacket.newBuilder()) {
participantIdentity = "server-api-sender"
user = with(UserPacket.newBuilder()) {
payload = ByteString.copyFrom("hello", Charsets.UTF_8)
build()
}
build()
}
val dataBuffer = DataChannel.Buffer(
ByteBuffer.wrap(dataPacket.toByteArray()),
true,
)

subDataChannel.observer?.onMessage(dataBuffer)
val events = collector.stopCollecting()

assertEquals(1, events.size)
val event = events[0] as RoomEvent.DataReceived

assertNull(event.participant)
assertEquals("server-api-sender", event.participantIdentity)
}
}
Loading