@@ -265,53 +265,112 @@ def __init__(
265265 def _init_connection (self ) -> None :
266266 """Initialize event loop, thread, and WebRTC connection."""
267267 # Start event loop in background thread
268- self ._loop = asyncio .new_event_loop ()
268+ loop = asyncio .new_event_loop ()
269+ self ._loop = loop
269270
270271 def _run (loop : asyncio .AbstractEventLoop ) -> None :
271272 asyncio .set_event_loop (loop )
272- loop .run_forever ()
273+ try :
274+ loop .run_forever ()
275+ finally :
276+ try :
277+ try :
278+ pending_tasks = asyncio .all_tasks (loop )
279+ for task in pending_tasks :
280+ task .cancel ()
281+ if pending_tasks :
282+ loop .run_until_complete (
283+ asyncio .gather (* pending_tasks , return_exceptions = True )
284+ )
285+ except Exception :
286+ logger .exception ("Failed to drain WebRTC event-loop tasks" )
273287
274- self ._loop_thread = threading .Thread (
275- target = _run , args = (self ._loop ,), daemon = True
276- )
277- self ._loop_thread .start ()
288+ try :
289+ loop .run_until_complete (loop .shutdown_asyncgens ())
290+ except Exception :
291+ logger .exception ("Failed to shut down WebRTC async generators" )
292+ finally :
293+ try :
294+ loop .close ()
295+ finally :
296+ asyncio .set_event_loop (None )
297+
298+ loop_thread = threading .Thread (target = _run , args = (loop ,), daemon = True )
299+ self ._loop_thread = loop_thread
300+ try :
301+ loop_thread .start ()
302+ except BaseException :
303+ if not loop_thread .is_alive ():
304+ loop .close ()
305+ self ._loop = None
306+ self ._loop_thread = None
307+ raise
278308
279309 # Initialize WebRTC connection
280- fut = asyncio .run_coroutine_threadsafe (self ._init (), self ._loop )
310+ init_coro = self ._init ()
311+ try :
312+ fut = asyncio .run_coroutine_threadsafe (init_coro , loop )
313+ except BaseException :
314+ init_coro .close ()
315+ raise
316+
281317 try :
282318 fut .result ()
283- except requests .exceptions .HTTPError as e :
284- if e .response .status_code == 404 :
285- raise RuntimeError (
286- f"WebRTC endpoint not found at { self ._api_url } /initialise_webrtc_worker.\n "
287- f"This API URL may not support WebRTC streaming.\n "
288- f"Troubleshooting:\n "
289- f" - For self-hosted inference, ensure the server is started with WebRTC enabled\n "
290- f" - For Roboflow Cloud, use a dedicated inference server URL (not serverless.roboflow.com)\n "
291- f" - Verify the --api-url parameter points to the correct server\n "
292- f"Response: { e .response .text } "
293- ) from e
294- else :
319+ except BaseException as error :
320+ if not fut .done ():
321+ fut .cancel ()
322+
323+ if isinstance (error , requests .exceptions .HTTPError ):
324+ if error .response .status_code == 404 :
325+ raise RuntimeError (
326+ f"WebRTC endpoint not found at { self ._api_url } /initialise_webrtc_worker.\n "
327+ f"This API URL may not support WebRTC streaming.\n "
328+ f"Troubleshooting:\n "
329+ f" - For self-hosted inference, ensure the server is started with WebRTC enabled\n "
330+ f" - For Roboflow Cloud, use a dedicated inference server URL (not serverless.roboflow.com)\n "
331+ f" - Verify the --api-url parameter points to the correct server\n "
332+ f"Response: { error .response .text } "
333+ ) from error
295334 raise RuntimeError (
296- f"Failed to initialize WebRTC session (HTTP { e .response .status_code } ).\n "
335+ f"Failed to initialize WebRTC session (HTTP { error .response .status_code } ).\n "
297336 f"API URL: { self ._api_url } \n "
298- f"Error: { e } \n "
299- f"Response: { e .response .text } "
300- ) from e
301- except Exception as e :
337+ f"Error: { error } \n "
338+ f"Response: { error .response .text } "
339+ ) from error
340+
341+ if not isinstance (error , Exception ):
342+ raise
343+
302344 raise RuntimeError (
303- f"Failed to initialize WebRTC session: { e .__class__ .__name__ } : { e } \n "
345+ f"Failed to initialize WebRTC session: "
346+ f"{ error .__class__ .__name__ } : { error } \n "
304347 f"API URL: { self ._api_url } "
305- ) from e
348+ ) from error
306349
307350 def _ensure_started (self ) -> None :
308351 """Ensure connection is started (thread-safe, idempotent)."""
309- with self ._state_lock :
310- if self ._state == SessionState .NOT_STARTED :
311- self ._state = SessionState .STARTED
312- self ._init_connection ()
313- elif self ._state == SessionState .CLOSED :
314- raise RuntimeError ("Cannot use closed WebRTCSession" )
352+ startup_failed = False
353+ try :
354+ with self ._state_lock :
355+ if self ._state == SessionState .NOT_STARTED :
356+ self ._state = SessionState .STARTED
357+ try :
358+ self ._init_connection ()
359+ except BaseException :
360+ self ._state = SessionState .CLOSED
361+ startup_failed = True
362+ raise
363+ elif self ._state == SessionState .CLOSED :
364+ raise RuntimeError ("Cannot use closed WebRTCSession" )
365+ except BaseException :
366+ if startup_failed :
367+ try :
368+ self .close ()
369+ except (Exception , asyncio .CancelledError ):
370+ logger .exception (
371+ "Failed to clean up WebRTC session after startup failure"
372+ )
373+ raise
315374
316375 def _parse_video_metadata (
317376 self ,
@@ -406,6 +465,8 @@ def _close_resources(self) -> None:
406465 try :
407466 with self ._state_lock :
408467 self ._state = SessionState .CLOSED
468+ loop = self ._loop
469+ loop_thread = self ._loop_thread
409470
410471 # Signal video iterator to stop by putting None sentinel
411472 try :
@@ -416,25 +477,61 @@ def _close_resources(self) -> None:
416477 # Cleanup resources (nested finally ensures all cleanup steps execute)
417478 try :
418479 # Close peer connection
419- if self ._loop and self ._pc :
420- asyncio .run_coroutine_threadsafe (
421- self ._pc .close (), self ._loop
422- ).result ()
480+ if (
481+ loop is not None
482+ and not loop .is_closed ()
483+ and loop_thread is not None
484+ and loop_thread .is_alive ()
485+ and self ._pc is not None
486+ ):
487+ peer_close_coro = self ._pc .close ()
488+ try :
489+ peer_close_future = asyncio .run_coroutine_threadsafe (
490+ peer_close_coro , loop
491+ )
492+ except BaseException :
493+ peer_close_coro .close ()
494+ raise
495+ peer_close_future .result ()
423496 finally :
424497 try :
425498 # Cleanup source (webcam, video file, etc.)
426- if self ._loop and self ._source :
427- asyncio .run_coroutine_threadsafe (
428- self ._source .cleanup (), self ._loop
429- ).result ()
499+ if (
500+ loop is not None
501+ and not loop .is_closed ()
502+ and loop_thread is not None
503+ and loop_thread .is_alive ()
504+ and self ._source is not None
505+ ):
506+ source_cleanup_coro = self ._source .cleanup ()
507+ try :
508+ source_cleanup_future = asyncio .run_coroutine_threadsafe (
509+ source_cleanup_coro , loop
510+ )
511+ except BaseException :
512+ source_cleanup_coro .close ()
513+ raise
514+ source_cleanup_future .result ()
430515 finally :
431516 # Stop event loop and join thread
432- if self ._loop :
433- self ._loop .call_soon_threadsafe (self ._loop .stop )
434- if self ._loop_thread :
435- self ._loop_thread .join (
436- timeout = WEBRTC_EVENT_LOOP_SHUTDOWN_TIMEOUT
437- )
517+ try :
518+ if loop is not None and not loop .is_closed ():
519+ try :
520+ loop .call_soon_threadsafe (loop .stop )
521+ except RuntimeError :
522+ if not loop .is_closed ():
523+ raise
524+ finally :
525+ if (
526+ loop_thread is not None
527+ and loop_thread is not threading .current_thread ()
528+ ):
529+ loop_thread .join (timeout = WEBRTC_EVENT_LOOP_SHUTDOWN_TIMEOUT )
530+ if loop_thread .is_alive ():
531+ logger .warning (
532+ "WebRTC event loop thread did not stop "
533+ f"within { WEBRTC_EVENT_LOOP_SHUTDOWN_TIMEOUT } s"
534+ )
438535 finally :
439536 self ._close_done .set ()
440537
@@ -969,6 +1066,7 @@ async def _init(self) -> None:
9691066 turn_config = await self ._get_turn_config ()
9701067
9711068 pc = RTCPeerConnection (configuration = turn_config )
1069+ self ._pc = pc
9721070 relay = MediaRelay ()
9731071
9741072 # Monitor ICE connection state for failures
@@ -1275,5 +1373,3 @@ def _on_data_message(message: Any) -> None: # noqa: ANN401
12751373 # Start video file upload if applicable
12761374 if isinstance (self ._source , VideoFileSource ):
12771375 asyncio .ensure_future (self ._source .start_upload ())
1278-
1279- self ._pc = pc
0 commit comments