| 4364 | |
| 4365 | # Create a new event loop for the WebRTC server in a separate thread |
| 4366 | def start_async_server(): |
| 4367 | loop = asyncio.new_event_loop() |
| 4368 | asyncio.set_event_loop(loop) |
| 4369 | # Store loop reference for pose streaming thread |
| 4370 | streamer_instance._webrtc_loop = loop |
| 4371 | |
| 4372 | async def run_peer_custom(reader, writer): |
| 4373 | from aiortc import RTCPeerConnection, RTCSessionDescription, RTCConfiguration, RTCIceServer |
| 4374 | |
| 4375 | client_addr = writer.get_extra_info('peername') |
| 4376 | streamer_instance._log(f"[WEBRTC] VisionOS client connected from {client_addr}", force=True) |
| 4377 | |
| 4378 | # Configure RTCPeerConnection for low latency |
| 4379 | # Configure RTCPeerConnection for low latency |
| 4380 | ice_servers = [] |
| 4381 | if streamer_instance._ice_servers: |
| 4382 | streamer_instance._log(f"[WEBRTC] Using {len(streamer_instance._ice_servers)} custom ICE servers from signaling") |
| 4383 | for s in streamer_instance._ice_servers: |
| 4384 | ice_servers.append(RTCIceServer( |
| 4385 | urls=s.get("urls"), |
| 4386 | username=s.get("username"), |
| 4387 | credential=s.get("credential") |
| 4388 | )) |
| 4389 | else: |
| 4390 | streamer_instance._log("[WEBRTC] Warning: No custom ICE servers, falling back to Google STUN") |
| 4391 | ice_servers = [RTCIceServer(urls=["stun:stun.l.google.com:19302"])] |
| 4392 | |
| 4393 | config = RTCConfiguration(iceServers=ice_servers) |
| 4394 | pc = RTCPeerConnection(configuration=config) |
| 4395 | |
| 4396 | @pc.on("iceconnectionstatechange") |
| 4397 | async def on_ice_state_change(): |
| 4398 | streamer_instance._log(f"[WEBRTC] ICE state ({client_addr}): {pc.iceConnectionState}", force=True) |
| 4399 | if pc.iceConnectionState == "connected": |
| 4400 | streamer_instance._log(f"[WEBRTC] Connection established ({client_addr}) - video should flow now", force=True) |
| 4401 | with streamer_instance._webrtc_connection_condition: |
| 4402 | streamer_instance._webrtc_connected = True |
| 4403 | streamer_instance._webrtc_connection_condition.notify_all() |
| 4404 | if pc.iceConnectionState in ("failed", "closed", "disconnected"): |
| 4405 | with streamer_instance._webrtc_connection_condition: |
| 4406 | streamer_instance._webrtc_connected = False |
| 4407 | if pc.iceConnectionState in ("failed", "closed"): |
| 4408 | await pc.close() |
| 4409 | writer.close() |
| 4410 | await writer.wait_closed() |
| 4411 | |
| 4412 | @pc.on("track") |
| 4413 | def on_track(track): |
| 4414 | streamer_instance._log(f"[WEBRTC] Track received: {track.kind}") |
| 4415 | |
| 4416 | # Create video track if video is configured |
| 4417 | if streamer_instance._video_config is not None: |
| 4418 | try: |
| 4419 | if device is None: |
| 4420 | streamer_instance._log("Creating synthetic video stream (no camera)...") |
| 4421 | if streamer_instance.frame_callback is None: |
| 4422 | streamer_instance._log("[VIDEO] Warning: No frame callback registered. Stream will be blank.", force=True) |
| 4423 | streamer_instance._log(" Use register_frame_callback() to generate frames.", force=True) |