From e87839044c6cfe86bcc13dfdc1a6200acdade78b Mon Sep 17 00:00:00 2001 From: Shijing Xian Date: Fri, 23 Oct 2026 15:28:13 -0700 Subject: [PATCH 1/4] Expose participantIdentity on DataReceivedEvent DataReceivedEvent carries a nullable RemoteParticipant, which is null whenever the sender is not in remoteParticipants - data sent through the Server API, or by a participant that has already left. In that case the receiver had no way at all to tell who sent the data, even though the engine had the identity in hand at the construction site. Add participantIdentity, populated from the data packet for every event. Port of livekit/client-sdk-js#2092. Co-Authored-By: Claude Opus 5 (1M context) --- lib/src/core/room.dart | 1 + lib/src/events.dart | 5 ++ test/core/data_received_identity_test.dart | 78 ++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 test/core/data_received_identity_test.dart diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index f10199abb..e851b4eda 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -1046,6 +1046,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { final event = DataReceivedEvent( participant: senderParticipant, + participantIdentity: dataPacketEvent.identity, data: dataPacketEvent.packet.payload, topic: dataPacketEvent.packet.topic, ); diff --git a/lib/src/events.dart b/lib/src/events.dart index 9195f452a..1e0a7f2de 100644 --- a/lib/src/events.dart +++ b/lib/src/events.dart @@ -450,10 +450,15 @@ class ParticipantConnectionQualityUpdatedEvent with RoomEvent, ParticipantEvent class DataReceivedEvent with RoomEvent, ParticipantEvent { /// Sender of the data. This may be null if data is sent from Server API. final RemoteParticipant? participant; + + /// Identity of the sender, which is reported even when [participant] is null + /// because the sender is not (or is no longer) a participant in this room. + final String participantIdentity; final List data; final String? topic; const DataReceivedEvent({ required this.participant, + required this.participantIdentity, required this.data, required this.topic, }); diff --git a/test/core/data_received_identity_test.dart b/test/core/data_received_identity_test.dart new file mode 100644 index 000000000..496d808b3 --- /dev/null +++ b/test/core/data_received_identity_test.dart @@ -0,0 +1,78 @@ +// Copyright 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@Timeout(Duration(seconds: 10)) +library; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:livekit_client/livekit_client.dart'; +import 'package:livekit_client/src/proto/livekit_models.pb.dart' as lk_models; +import '../mock/e2e_container.dart'; + +void main() { + late E2EContainer container; + late Room room; + + setUpAll(() async { + container = E2EContainer(); + room = container.room; + await container.connectRoom(); + await container.simulateRemoteParticipantJoin('remote-a'); + }); + + tearDownAll(() async { + await container.dispose(); + }); + + Future deliverFrom(String identity) async { + final completer = Completer(); + final cancel = room.events.on(completer.complete); + + container.deliverInboundDataPacket( + lk_models.DataPacket( + kind: lk_models.DataPacket_Kind.RELIABLE, + participantIdentity: identity, + user: lk_models.UserPacket( + payload: utf8.encode('hello'), + participantIdentity: identity, + ), + ), + ); + + final event = await completer.future.timeout(const Duration(seconds: 3)); + await cancel(); + return event; + } + + test('reports the sender identity when the participant is not in the room', () async { + // Data sent by the Server API, or by a participant that has already left, + // has no RemoteParticipant to attach to. Without the identity the receiver + // cannot tell who sent it at all. + final event = await deliverFrom('server-api-sender'); + + expect(event.participant, isNull); + expect(event.participantIdentity, 'server-api-sender'); + }); + + test('reports the sender identity for a known participant too', () async { + final event = await deliverFrom('remote-a'); + + expect(event.participant?.identity, 'remote-a'); + expect(event.participantIdentity, 'remote-a'); + }); +} From d2cbd5ac56ee94f90b052f668b41d808a31e8ae3 Mon Sep 17 00:00:00 2001 From: Shijing Xian Date: Fri, 23 Oct 2026 16:57:59 -0700 Subject: [PATCH 2/4] Add the release entry for DataReceivedEvent.participantIdentity Co-Authored-By: Claude Opus 5 (1M context) --- .changes/data-received-participant-identity | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/data-received-participant-identity diff --git a/.changes/data-received-participant-identity b/.changes/data-received-participant-identity new file mode 100644 index 000000000..37cc9db65 --- /dev/null +++ b/.changes/data-received-participant-identity @@ -0,0 +1 @@ +patch type="added" "`DataReceivedEvent.participantIdentity` reports the sender even when `participant` is null, which is the case for data sent through the Server API or by a participant that has already left" From a4ff1573c9df7bd6110a9dd7cde6e042d4d7db9c Mon Sep 17 00:00:00 2001 From: Shijing Xian Date: Fri, 23 Oct 2026 17:03:23 -0700 Subject: [PATCH 3/4] Default participantIdentity so the constructor stays source compatible A required named parameter breaks existing DataReceivedEvent callers and would make this a major release. The engine is the only producer and always passes the identity, so defaulting it keeps the change additive and the patch bump honest. Matches the Android port. Co-Authored-By: Claude Opus 5 (1M context) --- lib/src/events.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/events.dart b/lib/src/events.dart index 1e0a7f2de..df2e21d5d 100644 --- a/lib/src/events.dart +++ b/lib/src/events.dart @@ -458,9 +458,9 @@ class DataReceivedEvent with RoomEvent, ParticipantEvent { final String? topic; const DataReceivedEvent({ required this.participant, - required this.participantIdentity, required this.data, required this.topic, + this.participantIdentity = '', }); @override From 8527893852a61d05c6656e9fa2203fd0fe0daffc Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:55:50 +0800 Subject: [PATCH 4/4] nits Add TODO to tighten the DataReceivedEvent constructor in the next major Document when DataReceivedEvent.participantIdentity is empty Point DataReceivedEvent doc to participantIdentity --- .changes/data-received-participant-identity | 2 +- lib/src/events.dart | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.changes/data-received-participant-identity b/.changes/data-received-participant-identity index 37cc9db65..4312bc745 100644 --- a/.changes/data-received-participant-identity +++ b/.changes/data-received-participant-identity @@ -1 +1 @@ -patch type="added" "`DataReceivedEvent.participantIdentity` reports the sender even when `participant` is null, which is the case for data sent through the Server API or by a participant that has already left" +patch type="added" "`DataReceivedEvent.participantIdentity` reports the sender even when `participant` is null, for example when data arrives before the sender's join is announced or after the sender has left" diff --git a/lib/src/events.dart b/lib/src/events.dart index df2e21d5d..1ba8cf5d6 100644 --- a/lib/src/events.dart +++ b/lib/src/events.dart @@ -446,16 +446,21 @@ class ParticipantConnectionQualityUpdatedEvent with RoomEvent, ParticipantEvent /// Data received from [RemoteParticipant]. /// Data packets provides the ability to use LiveKit to send/receive arbitrary /// payloads. +/// When [participant] is null, use [participantIdentity] to tell who sent the +/// data. It is empty for data sent from Server API or by a hidden participant. /// Emitted by [Room] and [RemoteParticipant]. class DataReceivedEvent with RoomEvent, ParticipantEvent { /// Sender of the data. This may be null if data is sent from Server API. final RemoteParticipant? participant; - /// Identity of the sender, which is reported even when [participant] is null - /// because the sender is not (or is no longer) a participant in this room. + /// Identity of the sender. Set even when [participant] is null, for example + /// when the data arrives before the sender's join is announced or after the + /// sender has left. Empty for data sent from Server API or by a hidden + /// participant. final String participantIdentity; final List data; final String? topic; + // TODO: In the next major release, mark this constructor @internal and make participantIdentity required. const DataReceivedEvent({ required this.participant, required this.data, @@ -466,7 +471,7 @@ class DataReceivedEvent with RoomEvent, ParticipantEvent { @override String toString() => '${runtimeType}' - '(participant: ${participant}, topic: ${topic}, data: ${data})'; + '(participant: ${participant}, participantIdentity: ${participantIdentity}, topic: ${topic}, data: ${data})'; } /// The participant's isSpeaking property has changed