diff --git a/.changes/data-received-participant-identity b/.changes/data-received-participant-identity new file mode 100644 index 000000000..4312bc745 --- /dev/null +++ b/.changes/data-received-participant-identity @@ -0,0 +1 @@ +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/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..1ba8cf5d6 100644 --- a/lib/src/events.dart +++ b/lib/src/events.dart @@ -446,22 +446,32 @@ 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. 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, required this.topic, + this.participantIdentity = '', }); @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 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'); + }); +}