1111
1212from __future__ import annotations
1313
14- import asyncio
1514import os
16- import uuid
17- from typing import Callable , Optional
1815
1916import pytest
2017
21- from livekit import api , rtc
22- from livekit .rtc .room import EventTypes
18+ from livekit import rtc
2319
24-
25- WAIT_TIMEOUT = 20.0
26- WAIT_INTERVAL = 0.1
27-
28-
29- def skip_if_no_credentials ():
30- required_vars = ["LIVEKIT_URL" , "LIVEKIT_API_KEY" , "LIVEKIT_API_SECRET" ]
31- missing = [var for var in required_vars if not os .getenv (var )]
32- return pytest .mark .skipif (
33- bool (missing ), reason = f"Missing environment variables: { ', ' .join (missing )} "
34- )
35-
36-
37- def create_token (identity : str , room_name : str ) -> str :
38- return (
39- api .AccessToken ()
40- .with_identity (identity )
41- .with_name (identity )
42- .with_grants (
43- api .VideoGrants (
44- room_join = True ,
45- room = room_name ,
46- )
47- )
48- .to_jwt ()
49- )
50-
51-
52- def unique_room_name (base : str ) -> str :
53- return f"{ base } -{ uuid .uuid4 ().hex [:8 ]} "
54-
55-
56- async def _wait_until (
57- predicate : Callable [[], bool ],
58- * ,
59- timeout : float = WAIT_TIMEOUT ,
60- interval : float = WAIT_INTERVAL ,
61- message : str = "condition not met" ,
62- ) -> None :
63- loop = asyncio .get_event_loop ()
64- deadline = loop .time () + timeout
65- while loop .time () < deadline :
66- if predicate ():
67- return
68- await asyncio .sleep (interval )
69- raise AssertionError (f"timeout waiting: { message } " )
70-
71-
72- async def _connect (room : rtc .Room , identity : str , room_name : str ) -> str :
73- """Mints a token, connects `room`, and returns the token (for reconnect)."""
74- token = create_token (identity , room_name )
75- url = os .environ ["LIVEKIT_URL" ]
76- await room .connect (url , token )
77- return token
78-
79-
80- async def _ensure_all_connected (rooms : list [rtc .Room ]) -> None :
81- await _wait_until (
82- lambda : all (r .connection_state == rtc .ConnectionState .CONN_CONNECTED for r in rooms ),
83- message = "not all rooms reached CONN_CONNECTED" ,
84- )
85-
86-
87- async def _ensure_track_subscribed (room : rtc .Room , track_sid : str ) -> None :
88- def _has_subscribed () -> bool :
89- for participant in room .remote_participants .values ():
90- pub = participant .track_publications .get (track_sid )
91- if pub is not None and pub .subscribed :
92- return True
93- return False
94-
95- await _wait_until (
96- _has_subscribed ,
97- message = f"room did not subscribe to track { track_sid } " ,
98- )
99-
100-
101- def _expect_event (
102- room : rtc .Room ,
103- event : EventTypes ,
104- predicate : Optional [Callable [..., bool ]] = None ,
105- ) -> asyncio .Future :
106- """Returns a future that resolves when `event` (optionally matching
107- `predicate`) is fired on `room`."""
108- loop = asyncio .get_event_loop ()
109- fut : asyncio .Future = loop .create_future ()
110-
111- def _on_event (* args , ** kwargs ) -> None :
112- if fut .done ():
113- return
114- if predicate is None or predicate (* args , ** kwargs ):
115- fut .set_result (args )
116-
117- room .on (event , _on_event )
118- return fut
119-
120-
121- async def _await_event (fut : asyncio .Future , timeout : float = WAIT_TIMEOUT ) -> None :
122- try :
123- await asyncio .wait_for (fut , timeout = timeout )
124- except asyncio .TimeoutError as e :
125- raise AssertionError ("timed out waiting for event" ) from e
20+ from utils import ( # type: ignore[import-not-found]
21+ await_event ,
22+ connect_room ,
23+ create_token ,
24+ ensure_rooms_all_connected ,
25+ ensure_track_subscribed ,
26+ expect_room_event ,
27+ skip_if_no_credentials ,
28+ unique_room_name ,
29+ wait_until ,
30+ )
12631
12732
12833async def _publish_video (room : rtc .Room , track_name : str ) -> rtc .LocalTrackPublication :
@@ -139,40 +44,39 @@ async def _publish_audio(room: rtc.Room, track_name: str) -> rtc.LocalTrackPubli
13944 return await room .local_participant .publish_track (track , options )
14045
14146
142- @skip_if_no_credentials ()
47+ @skip_if_no_credentials () # type: ignore[untyped-decorator]
14348@pytest .mark .asyncio
14449async def test_connection_basics () -> None :
14550 room_name = unique_room_name ("py-basics" )
14651
147- p1 , p2 = rtc .Room (), rtc .Room ()
148- await _connect (p1 , "p1" , room_name )
149- await _connect (p2 , "p2" , room_name )
150- await _ensure_all_connected ([p1 , p2 ])
52+ p1 = await connect_room ("p1" , room_name )
53+ p2 = await connect_room ("p2" , room_name )
54+ await ensure_rooms_all_connected ([p1 , p2 ])
15155
15256 # p2 should observe p1 leaving
153- p2_saw_p1_left = _expect_event (
57+ p2_saw_p1_left = expect_room_event (
15458 p2 ,
15559 "participant_disconnected" ,
15660 predicate = lambda p : p .identity == "p1" ,
15761 )
15862 await p1 .disconnect ()
159- await _await_event (p2_saw_p1_left )
63+ await await_event (p2_saw_p1_left )
16064
161- await _wait_until (
65+ await wait_until (
16266 lambda : p1 .connection_state == rtc .ConnectionState .CONN_DISCONNECTED ,
16367 message = "p1 did not reach CONN_DISCONNECTED" ,
16468 )
16569
16670 await p2 .disconnect ()
167- await _wait_until (
71+ await wait_until (
16872 lambda : p2 .connection_state == rtc .ConnectionState .CONN_DISCONNECTED ,
16973 message = "p2 did not reach CONN_DISCONNECTED" ,
17074 )
17175
17276 # p3: connect, disconnect, reconnect, disconnect cycle
173- p3 = rtc .Room ()
174- p3_token = await _connect (p3 , "p3" , room_name )
77+ p3 = await connect_room ("p3" , room_name )
17578 p3_url = os .environ ["LIVEKIT_URL" ]
79+ p3_token = create_token ("p3" , room_name )
17680
17781 await p3 .disconnect ()
17882 assert p3 .connection_state == rtc .ConnectionState .CONN_DISCONNECTED , (
@@ -190,29 +94,28 @@ async def test_connection_basics() -> None:
19094 )
19195
19296 # p4 joins, then p3 reconnects to publish to p4
193- p4 = rtc .Room ()
194- await _connect (p4 , "p4" , room_name )
97+ p4 = await connect_room ("p4" , room_name )
19598
19699 await p3 .connect (p3_url , p3_token )
197100 assert p3 .connection_state == rtc .ConnectionState .CONN_CONNECTED , (
198101 f"expected p3 reconnected, got { p3 .connection_state } "
199102 )
200103
201104 # publish camera from p3, expect p4 to see track_published
202- video_published = _expect_event (p4 , "track_published" )
105+ video_published = expect_room_event (p4 , "track_published" )
203106 video_pub = await _publish_video (p3 , "p3-camera" )
204- await _await_event (video_published )
205- await _ensure_track_subscribed (p4 , video_pub .sid )
107+ await await_event (video_published )
108+ await ensure_track_subscribed (p4 , video_pub .sid )
206109
207110 # publish microphone from p3, expect p4 to see another track_published
208- audio_published = _expect_event (
111+ audio_published = expect_room_event (
209112 p4 ,
210113 "track_published" ,
211114 predicate = lambda pub , _p : pub .sid != video_pub .sid ,
212115 )
213116 audio_pub = await _publish_audio (p3 , "p3-mic" )
214- await _await_event (audio_published )
215- await _ensure_track_subscribed (p4 , audio_pub .sid )
117+ await await_event (audio_published )
118+ await ensure_track_subscribed (p4 , audio_pub .sid )
216119
217120 await p3 .disconnect ()
218121 await p4 .disconnect ()
0 commit comments