MacBook pro commited on
Commit
6472b34
·
1 Parent(s): 6efdafb

feat: add /webrtc/negotiations and /webrtc/ice_gather_test diagnostics

Browse files
Files changed (1) hide show
  1. webrtc_server.py +77 -0
webrtc_server.py CHANGED
@@ -493,6 +493,7 @@ class PeerState:
493
  _peer_state: Optional[PeerState] = None
494
  _peer_lock = asyncio.Lock()
495
  _last_peer_snapshot: Optional[dict[str, Any]] = None
 
496
 
497
 
498
  class IncomingVideoTrack(MediaStreamTrack):
@@ -899,6 +900,14 @@ async def webrtc_offer(offer: Dict[str, Any], x_api_key: Optional[str] = Header(
899
 
900
  def stage(msg: str, level: str = "info"):
901
  line = f"[{negotiation_id}] {msg}"
 
 
 
 
 
 
 
 
902
  if level == "error":
903
  logger.error(line)
904
  elif level == "warning":
@@ -1496,6 +1505,74 @@ async def webrtc_offer(offer: Dict[str, Any], x_api_key: Optional[str] = Header(
1496
  payload["negotiation_id"] = negotiation_id
1497
  return payload
1498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1499
 
1500
  @router.get("/token")
1501
  async def mint_token():
 
493
  _peer_state: Optional[PeerState] = None
494
  _peer_lock = asyncio.Lock()
495
  _last_peer_snapshot: Optional[dict[str, Any]] = None
496
+ _negotiation_events: list[dict[str, Any]] = [] # rolling recent negotiation stage events
497
 
498
 
499
  class IncomingVideoTrack(MediaStreamTrack):
 
900
 
901
  def stage(msg: str, level: str = "info"):
902
  line = f"[{negotiation_id}] {msg}"
903
+ evt = {"ts": time.time(), "negotiation_id": negotiation_id, "msg": msg, "level": level}
904
+ try:
905
+ _negotiation_events.append(evt)
906
+ # Keep only last 80 events
907
+ if len(_negotiation_events) > 80:
908
+ del _negotiation_events[: len(_negotiation_events) - 80]
909
+ except Exception:
910
+ pass
911
  if level == "error":
912
  logger.error(line)
913
  elif level == "warning":
 
1505
  payload["negotiation_id"] = negotiation_id
1506
  return payload
1507
 
1508
+ @router.get("/negotiations")
1509
+ async def negotiations():
1510
+ """Return recent negotiation stage events and last peer snapshot.
1511
+
1512
+ Useful when /webrtc/ice_stats shows inactive (active:false) to see how far
1513
+ the last negotiation progressed before failure.
1514
+ """
1515
+ try:
1516
+ return {
1517
+ "events": _negotiation_events[-50:], # last 50 for brevity
1518
+ "last_peer_snapshot": _last_peer_snapshot,
1519
+ "active": _peer_state is not None,
1520
+ }
1521
+ except Exception as e:
1522
+ return {"error": str(e)}
1523
+
1524
+ @router.get("/ice_gather_test")
1525
+ async def ice_gather_test():
1526
+ """Perform a standalone ICE gathering cycle (no media) to enumerate local candidates.
1527
+
1528
+ This helps distinguish TURN credential / gathering failures from SDP / negotiation issues.
1529
+ """
1530
+ if not AIORTC_AVAILABLE:
1531
+ raise HTTPException(status_code=503, detail="aiortc unavailable")
1532
+ try:
1533
+ cfg = _ice_configuration()
1534
+ pc = RTCPeerConnection(configuration=cfg)
1535
+ # Create a dummy data channel to ensure ICE starts
1536
+ try:
1537
+ pc.createDataChannel("probe")
1538
+ except Exception:
1539
+ pass
1540
+ offer = await pc.createOffer()
1541
+ await pc.setLocalDescription(offer)
1542
+ # Wait for iceGatheringState complete or timeout
1543
+ t0 = time.time()
1544
+ while pc.iceGatheringState != 'complete' and (time.time() - t0) < 6.0:
1545
+ await asyncio.sleep(0.2)
1546
+ stats = await pc.getStats()
1547
+ summary = {
1548
+ 'local_candidates': 0,
1549
+ 'local_types': {},
1550
+ 'elapsed_ms': int((time.time() - t0) * 1000),
1551
+ 'gathering_state': pc.iceGatheringState,
1552
+ }
1553
+ candidate_details = []
1554
+ for sid, rep in stats.items():
1555
+ tp = getattr(rep, 'type', None)
1556
+ if tp == 'local-candidate':
1557
+ summary['local_candidates'] += 1
1558
+ ctype = getattr(rep, 'candidateType', 'unknown')
1559
+ summary['local_types'][ctype] = summary['local_types'].get(ctype, 0) + 1
1560
+ if len(candidate_details) < 25: # cap to avoid huge payloads
1561
+ candidate_details.append({
1562
+ 'type': ctype,
1563
+ 'protocol': getattr(rep, 'protocol', None),
1564
+ 'address': getattr(rep, 'address', None),
1565
+ 'port': getattr(rep, 'port', None),
1566
+ })
1567
+ try:
1568
+ await pc.close()
1569
+ except Exception:
1570
+ pass
1571
+ summary['candidates'] = candidate_details
1572
+ return summary
1573
+ except Exception as e:
1574
+ return {"error": str(e)}
1575
+
1576
 
1577
  @router.get("/token")
1578
  async def mint_token():