Files
2026-07-13 13:39:38 +08:00

336 lines
12 KiB
Python

from __future__ import annotations
import asyncio
from typing import Literal, overload
from livekit import rtc
from ..types import ATTRIBUTE_AGENT_NAME
async def wait_for_agent(
room: rtc.Room,
*,
agent_name: str | None = None,
) -> rtc.RemoteParticipant:
"""
Wait for an agent participant to join the room.
Args:
room: The room to wait for the agent in.
agent_name: If provided, waits for an agent with matching lk.agent.name attribute.
If None, returns the first agent participant found.
Returns:
The agent participant.
Raises:
RuntimeError: If the room is not connected.
"""
if not room.isconnected():
raise RuntimeError("room is not connected")
fut: asyncio.Future[rtc.RemoteParticipant] = asyncio.Future()
def matches_agent(p: rtc.RemoteParticipant) -> bool:
if p.kind != rtc.ParticipantKind.PARTICIPANT_KIND_AGENT:
return False
if agent_name is None:
return True
return p.attributes.get(ATTRIBUTE_AGENT_NAME) == agent_name
def on_participant_connected(p: rtc.RemoteParticipant) -> None:
if matches_agent(p) and not fut.done():
fut.set_result(p)
def on_attributes_changed(changed: list[str], p: rtc.Participant) -> None:
if isinstance(p, rtc.RemoteParticipant) and matches_agent(p) and not fut.done():
fut.set_result(p)
def on_connection_state_changed(state: int) -> None:
if state == rtc.ConnectionState.CONN_DISCONNECTED and not fut.done():
fut.set_exception(RuntimeError("room disconnected while waiting for agent participant"))
room.on("participant_connected", on_participant_connected)
room.on("participant_attributes_changed", on_attributes_changed)
room.on("connection_state_changed", on_connection_state_changed)
try:
# Check existing participants
for p in room.remote_participants.values():
if matches_agent(p):
return p
return await fut
finally:
room.off("participant_connected", on_participant_connected)
room.off("participant_attributes_changed", on_attributes_changed)
room.off("connection_state_changed", on_connection_state_changed)
async def wait_for_participant_attribute(
room: rtc.Room,
*,
identity: str,
attribute: str,
value: str,
) -> None:
"""Wait until a remote participant's attribute equals ``value``.
Returns immediately if the attribute is already set. Raises
:class:`RuntimeError` if the room is not connected, the participant is not
present, the participant disconnects, or the room disconnects before the
attribute is set.
"""
if not room.isconnected():
raise RuntimeError("room is not connected")
if identity not in room.remote_participants:
raise RuntimeError(f"participant {identity!r} is not in the room")
fut: asyncio.Future[None] = asyncio.Future()
def _is_match(p: rtc.Participant) -> bool:
return (
isinstance(p, rtc.RemoteParticipant)
and p.identity == identity
and p.attributes.get(attribute) == value
)
def _on_attributes_changed(_changed: list[str], p: rtc.Participant) -> None:
if _is_match(p) and not fut.done():
fut.set_result(None)
def _on_participant_disconnected(p: rtc.RemoteParticipant) -> None:
if p.identity == identity and not fut.done():
fut.set_exception(
RuntimeError(f"participant {identity!r} disconnected while waiting for {attribute}")
)
def _on_connection_state_changed(state: int) -> None:
if state == rtc.ConnectionState.CONN_DISCONNECTED and not fut.done():
fut.set_exception(
RuntimeError(f"room disconnected while waiting for {identity!r} {attribute}")
)
room.on("participant_attributes_changed", _on_attributes_changed)
room.on("participant_disconnected", _on_participant_disconnected)
room.on("connection_state_changed", _on_connection_state_changed)
try:
# defensive double check
existing = room.remote_participants.get(identity)
if existing is None:
raise RuntimeError(f"participant {identity!r} is not in the room")
if existing.attributes.get(attribute) == value:
return
await fut
finally:
room.off("participant_attributes_changed", _on_attributes_changed)
room.off("participant_disconnected", _on_participant_disconnected)
room.off("connection_state_changed", _on_connection_state_changed)
@overload
async def wait_for_participant(
room: rtc.Room,
*,
identity: str | None = None,
kind: list[rtc.ParticipantKind.ValueType] | rtc.ParticipantKind.ValueType | None = None,
include_local: Literal[False] = False,
) -> rtc.RemoteParticipant: ...
@overload
async def wait_for_participant(
room: rtc.Room,
*,
identity: str | None = None,
kind: list[rtc.ParticipantKind.ValueType] | rtc.ParticipantKind.ValueType | None = None,
include_local: Literal[True],
) -> rtc.Participant: ...
async def wait_for_participant(
room: rtc.Room,
*,
identity: str | None = None,
kind: list[rtc.ParticipantKind.ValueType] | rtc.ParticipantKind.ValueType | None = None,
include_local: bool = False,
) -> rtc.Participant:
"""
Returns a participant that matches the given identity. If identity is None, the first
participant that joins the room will be returned.
If the participant has already joined, the function will return immediately.
When `include_local` is True, the local participant is also considered.
"""
if not room.isconnected():
raise RuntimeError("room is not connected")
fut = asyncio.Future[rtc.Participant]()
def kind_match(p: rtc.Participant) -> bool:
if kind is None:
return True
if isinstance(kind, list):
return p.kind in kind
return p.kind == kind
def _on_participant_active(p: rtc.RemoteParticipant) -> None:
if (identity is None or p.identity == identity) and kind_match(p):
if not fut.done():
fut.set_result(p)
def _on_connection_state_changed(state: int) -> None:
if state == rtc.ConnectionState.CONN_DISCONNECTED and not fut.done():
fut.set_exception(RuntimeError("room disconnected while waiting for participant"))
room.on("participant_active", _on_participant_active)
room.on("connection_state_changed", _on_connection_state_changed)
try:
if include_local:
local = room.local_participant
if (identity is None or local.identity == identity) and kind_match(local):
return local
for p in room.remote_participants.values():
if p.state == rtc.ParticipantState.PARTICIPANT_STATE_ACTIVE:
_on_participant_active(p)
if fut.done():
break
return await fut
finally:
room.off("participant_active", _on_participant_active)
room.off("connection_state_changed", _on_connection_state_changed)
@overload
async def wait_for_track_publication(
room: rtc.Room,
*,
identity: str | None = None,
kind: list[rtc.TrackKind.ValueType] | rtc.TrackKind.ValueType | None = None,
include_local: Literal[False] = False,
wait_for_subscription: bool = False,
) -> rtc.RemoteTrackPublication: ...
@overload
async def wait_for_track_publication(
room: rtc.Room,
*,
identity: str | None = None,
kind: list[rtc.TrackKind.ValueType] | rtc.TrackKind.ValueType | None = None,
include_local: Literal[True],
wait_for_subscription: bool = False,
) -> rtc.TrackPublication: ...
async def wait_for_track_publication(
room: rtc.Room,
*,
identity: str | None = None,
kind: list[rtc.TrackKind.ValueType] | rtc.TrackKind.ValueType | None = None,
include_local: bool = False,
wait_for_subscription: bool = False,
) -> rtc.TrackPublication:
"""Returns a track publication matching the given identity and kind.
If identity is None, the first track matching the kind will be returned.
If a matching track is already published (and subscribed when
``wait_for_subscription`` is set), the function returns immediately.
When `include_local` is True, tracks published by the local participant are also considered;
local publications resolve on publish and ignore ``wait_for_subscription``.
"""
if not room.isconnected():
raise RuntimeError("room is not connected")
fut = asyncio.Future[rtc.TrackPublication]()
def kind_match(k: rtc.TrackKind.ValueType) -> bool:
if kind is None:
return True
if isinstance(kind, list):
return k in kind
return k == kind
def _matches(
publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant
) -> bool:
if not (identity is None or participant.identity == identity):
return False
if not kind_match(publication.kind):
return False
if wait_for_subscription and not (publication.subscribed and publication.track is not None):
return False
return True
def _on_track_published(
publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant
) -> None:
if not fut.done() and _matches(publication, participant):
fut.set_result(publication)
def _on_track_subscribed(
track: rtc.Track,
publication: rtc.RemoteTrackPublication,
participant: rtc.RemoteParticipant,
) -> None:
if not fut.done() and _matches(publication, participant):
fut.set_result(publication)
def _on_local_track_published(
publication: rtc.LocalTrackPublication, _track: rtc.Track
) -> None:
if fut.done():
return
local = room.local_participant
if (identity is None or local.identity == identity) and kind_match(publication.kind):
fut.set_result(publication)
def _on_connection_state_changed(state: int) -> None:
if state == rtc.ConnectionState.CONN_DISCONNECTED and not fut.done():
fut.set_exception(RuntimeError("room disconnected while waiting for track publication"))
if wait_for_subscription:
room.on("track_subscribed", _on_track_subscribed)
else:
room.on("track_published", _on_track_published)
if include_local:
room.on("local_track_published", _on_local_track_published)
room.on("connection_state_changed", _on_connection_state_changed)
try:
if include_local:
local = room.local_participant
if identity is None or local.identity == identity:
for local_publication in local.track_publications.values():
if kind_match(local_publication.kind):
return local_publication
for p in room.remote_participants.values():
for publication in p.track_publications.values():
if _matches(publication, p):
fut.set_result(publication)
break
if fut.done():
break
return await fut
finally:
if wait_for_subscription:
room.off("track_subscribed", _on_track_subscribed)
else:
room.off("track_published", _on_track_published)
if include_local:
room.off("local_track_published", _on_local_track_published)
room.off("connection_state_changed", _on_connection_state_changed)