336 lines
12 KiB
Python
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)
|