diff --git a/livekit-rtc/livekit/rtc/__init__.py b/livekit-rtc/livekit/rtc/__init__.py index 39b23feb..cc9c1b1e 100644 --- a/livekit-rtc/livekit/rtc/__init__.py +++ b/livekit-rtc/livekit/rtc/__init__.py @@ -20,7 +20,7 @@ from ._proto import stats_pb2 as stats from ._proto.e2ee_pb2 import EncryptionState, EncryptionType -from ._proto.participant_pb2 import ParticipantKind, DisconnectReason +from ._proto.participant_pb2 import ParticipantKind, ParticipantState, DisconnectReason from ._proto.room_pb2 import ( ConnectionQuality, ConnectionState, @@ -145,6 +145,7 @@ "LocalParticipant", "Participant", "ParticipantKind", + "ParticipantState", "DisconnectReason", "RemoteParticipant", "ConnectError", diff --git a/livekit-rtc/livekit/rtc/participant.py b/livekit-rtc/livekit/rtc/participant.py index f2cc206e..150477d5 100644 --- a/livekit-rtc/livekit/rtc/participant.py +++ b/livekit-rtc/livekit/rtc/participant.py @@ -16,6 +16,7 @@ import ctypes import asyncio +import datetime import os import mimetypes import aiofiles @@ -126,6 +127,20 @@ def kind(self) -> proto_participant.ParticipantKind.ValueType: """Participant's kind (e.g., regular participant, ingress, egress, sip, agent).""" return self._info.kind + @property + def state(self) -> proto_participant.ParticipantState.ValueType: + """Participant's connection state (joining, joined, active, disconnected).""" + return self._info.state + + @property + def joined_at(self) -> datetime.datetime | None: + """Timestamp of when the participant joined the room, or None if not yet joined.""" + if self._info.joined_at == 0: + return None + return datetime.datetime.fromtimestamp( + self._info.joined_at / 1000, tz=datetime.timezone.utc + ) + @property def permissions(self) -> proto_participant.ParticipantPermission: """The participant's permissions within the room.""" diff --git a/livekit-rtc/livekit/rtc/room.py b/livekit-rtc/livekit/rtc/room.py index a815bcd2..7fd778ae 100644 --- a/livekit-rtc/livekit/rtc/room.py +++ b/livekit-rtc/livekit/rtc/room.py @@ -49,6 +49,7 @@ EventTypes = Literal[ "participant_connected", "participant_disconnected", + "participant_active", "local_track_published", "local_track_unpublished", "local_track_subscribed", @@ -339,6 +340,8 @@ def on(self, event: EventTypes, callback: Optional[Callable] = None) -> Callable - Arguments: `participant` (RemoteParticipant) - **"participant_disconnected"**: Called when a participant leaves the room. - Arguments: `participant` (RemoteParticipant) + - **"participant_active"**: Called when a remote participant becomes active and is ready to receive data messages. + - Arguments: `participant` (RemoteParticipant) - **"local_track_published"**: Called when a local track is published. - Arguments: `publication` (LocalTrackPublication), `track` (Track) - **"local_track_unpublished"**: Called when a local track is unpublished. @@ -581,7 +584,7 @@ def unregister_text_stream_handler(self, topic: str): self._text_stream_handlers.pop(topic) async def disconnect( - self, *, reason: DisconnectReason = DisconnectReason.CLIENT_INITIATED + self, *, reason: DisconnectReason.ValueType = DisconnectReason.CLIENT_INITIATED ) -> None: """Disconnects from the room.""" if not self.isconnected(): @@ -667,6 +670,11 @@ def _on_room_event(self, event: proto_room.RoomEvent): rparticipant = self._remote_participants.pop(identity) rparticipant._info.disconnect_reason = event.participant_disconnected.disconnect_reason self.emit("participant_disconnected", rparticipant) + elif which == "participant_active": + rp = self._retrieve_remote_participant(event.participant_active.participant_identity) + if rp: + rp._info.state = proto_participant.PARTICIPANT_STATE_ACTIVE + self.emit("participant_active", rp) elif which == "local_track_published": sid = event.local_track_published.track_sid lpublication = self.local_participant.track_publications[sid]