Spaces:
Paused
Paused
MacBook pro commited on
Commit ·
6472b34
1
Parent(s): 6efdafb
feat: add /webrtc/negotiations and /webrtc/ice_gather_test diagnostics
Browse files- 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():
|