352 lines
12 KiB
Python
352 lines
12 KiB
Python
"""
|
|
WebRTC Signaling Handlers
|
|
|
|
This module contains WebRTC signaling message handlers for peer-to-peer communication.
|
|
Handles ICE candidate relay and session description exchange between peers.
|
|
"""
|
|
|
|
from typing import Any, Dict, TYPE_CHECKING
|
|
from fastapi import WebSocket
|
|
|
|
from logger import logger
|
|
from core.error_handling import (
|
|
with_webrtc_error_handling,
|
|
WebRTCError,
|
|
ErrorSeverity,
|
|
error_handler
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from core.session_manager import Session
|
|
from core.lobby_manager import Lobby
|
|
|
|
|
|
class WebRTCSignalingHandlers:
|
|
"""WebRTC signaling message handlers for peer-to-peer communication."""
|
|
|
|
@staticmethod
|
|
@with_webrtc_error_handling
|
|
async def handle_relay_ice_candidate(
|
|
websocket: WebSocket,
|
|
session: "Session",
|
|
lobby: "Lobby",
|
|
data: Dict[str, Any]
|
|
) -> None:
|
|
"""
|
|
Handle ICE candidate relay between peers.
|
|
|
|
Args:
|
|
websocket: The WebSocket connection
|
|
session: The sender session
|
|
lobby: The lobby context
|
|
data: Message data containing peer_id and candidate
|
|
"""
|
|
logger.info(f"{session.getName()} <- relayICECandidate")
|
|
|
|
if not data:
|
|
logger.error(f"{session.getName()} - relayICECandidate missing data")
|
|
await websocket.send_json({
|
|
"type": "error",
|
|
"data": {"error": "relayICECandidate missing data"}
|
|
})
|
|
return
|
|
|
|
# Check if session is properly joined to lobby with RTC peers
|
|
with session.session_lock:
|
|
if (lobby.id not in session.lobby_peers or
|
|
session.id not in lobby.sessions):
|
|
logger.error(
|
|
f"{session.short}:{session.name} <- relayICECandidate - "
|
|
f"Not an RTC peer ({session.id})"
|
|
)
|
|
await websocket.send_json({
|
|
"type": "error",
|
|
"data": {"error": "Not joined to lobby"}
|
|
})
|
|
return
|
|
|
|
session_peers = session.lobby_peers[lobby.id]
|
|
|
|
# Validate peer_id
|
|
peer_id = data.get("peer_id")
|
|
if peer_id not in session_peers:
|
|
logger.error(
|
|
f"{session.getName()} <- relayICECandidate - "
|
|
f"Not an RTC peer({peer_id}) in {session_peers}"
|
|
)
|
|
await websocket.send_json({
|
|
"type": "error",
|
|
"data": {"error": f"Target peer {peer_id} not found"}
|
|
})
|
|
return
|
|
|
|
# Get candidate data
|
|
candidate = data.get("candidate")
|
|
|
|
# Prepare message for target peer
|
|
message: Dict[str, Any] = {
|
|
"type": "iceCandidate",
|
|
"data": {
|
|
"peer_id": session.id,
|
|
"peer_name": session.name,
|
|
"candidate": candidate,
|
|
},
|
|
}
|
|
|
|
# Find target peer session and relay the message
|
|
peer_session = lobby.getSession(peer_id)
|
|
if not peer_session or not peer_session.ws:
|
|
logger.warning(
|
|
f"{session.getName()} - Live peer session {peer_id} "
|
|
f"not found in lobby {lobby.getName()}."
|
|
)
|
|
return
|
|
|
|
logger.info(
|
|
f"{session.getName()} -> iceCandidate({peer_session.getName()})"
|
|
)
|
|
|
|
try:
|
|
await peer_session.ws.send_json(message)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to relay ICE candidate: {e}")
|
|
|
|
@staticmethod
|
|
@with_webrtc_error_handling
|
|
async def handle_relay_session_description(
|
|
websocket: WebSocket,
|
|
session: "Session",
|
|
lobby: "Lobby",
|
|
data: Dict[str, Any]
|
|
) -> None:
|
|
"""
|
|
Handle session description relay between peers.
|
|
|
|
Args:
|
|
websocket: The WebSocket connection
|
|
session: The sender session
|
|
lobby: The lobby context
|
|
data: Message data containing peer_id and session_description
|
|
"""
|
|
logger.info(f"{session.getName()} <- relaySessionDescription")
|
|
|
|
if not data:
|
|
logger.error(f"{session.getName()} - relaySessionDescription missing data")
|
|
await websocket.send_json({
|
|
"type": "error",
|
|
"data": {"error": "relaySessionDescription missing data"}
|
|
})
|
|
return
|
|
|
|
# Check if session is properly joined to lobby with RTC peers
|
|
with session.session_lock:
|
|
if (lobby.id not in session.lobby_peers or
|
|
session.id not in lobby.sessions):
|
|
logger.error(
|
|
f"{session.short}:{session.name} <- relaySessionDescription - "
|
|
f"Not an RTC peer ({session.id})"
|
|
)
|
|
await websocket.send_json({
|
|
"type": "error",
|
|
"data": {"error": "Not joined to lobby"}
|
|
})
|
|
return
|
|
|
|
lobby_peers = session.lobby_peers[lobby.id]
|
|
|
|
# Validate peer_id
|
|
peer_id = data.get("peer_id")
|
|
if not peer_id:
|
|
logger.error(f"{session.getName()} - relaySessionDescription missing peer_id")
|
|
await websocket.send_json({
|
|
"type": "error",
|
|
"data": {"error": "relaySessionDescription missing peer_id"}
|
|
})
|
|
return
|
|
|
|
if peer_id not in lobby_peers:
|
|
logger.error(
|
|
f"{session.getName()} <- relaySessionDescription - "
|
|
f"Not an RTC peer({peer_id}) in {lobby_peers}"
|
|
)
|
|
await websocket.send_json({
|
|
"type": "error",
|
|
"data": {"error": f"Target peer {peer_id} not found"}
|
|
})
|
|
return
|
|
|
|
# Find target peer session
|
|
peer_session = lobby.getSession(peer_id)
|
|
if not peer_session or not peer_session.ws:
|
|
logger.warning(
|
|
f"{session.getName()} - Live peer session {peer_id} "
|
|
f"not found in lobby {lobby.getName()}."
|
|
)
|
|
return
|
|
|
|
# Get session description data
|
|
session_description = data.get("session_description")
|
|
|
|
# Prepare message for target peer
|
|
message: Dict[str, Any] = {
|
|
"type": "sessionDescription",
|
|
"data": {
|
|
"peer_id": session.id,
|
|
"peer_name": session.name,
|
|
"session_description": session_description,
|
|
},
|
|
}
|
|
|
|
logger.info(
|
|
f"{session.getName()} -> sessionDescription({peer_session.getName()})"
|
|
)
|
|
|
|
try:
|
|
await peer_session.ws.send_json(message)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to relay session description: {e}")
|
|
|
|
@staticmethod
|
|
@with_webrtc_error_handling
|
|
async def handle_add_peer(
|
|
session: "Session",
|
|
peer_session: "Session",
|
|
lobby: "Lobby"
|
|
) -> None:
|
|
"""
|
|
Handle adding WebRTC peer connections between two sessions in a lobby.
|
|
|
|
Args:
|
|
session: The session joining the lobby
|
|
peer_session: The existing peer session in the lobby
|
|
lobby: The lobby context
|
|
"""
|
|
# Only establish WebRTC connections if at least one has media
|
|
if session.has_media or peer_session.has_media:
|
|
# Add peer_session to session's peer list
|
|
with session.session_lock:
|
|
if lobby.id not in session.lobby_peers:
|
|
session.lobby_peers[lobby.id] = []
|
|
session.lobby_peers[lobby.id].append(peer_session.id)
|
|
|
|
# Add session to peer_session's peer list
|
|
with peer_session.session_lock:
|
|
if lobby.id not in peer_session.lobby_peers:
|
|
peer_session.lobby_peers[lobby.id] = []
|
|
peer_session.lobby_peers[lobby.id].append(session.id)
|
|
|
|
# Notify existing peer about new peer (they should not create offer)
|
|
logger.info(
|
|
f"{session.getName()} -> {peer_session.getName()}:addPeer("
|
|
f"{session.getName()}, {lobby.getName()}, should_create_offer=False, "
|
|
f"has_media={session.has_media})"
|
|
)
|
|
try:
|
|
if peer_session.ws:
|
|
await peer_session.ws.send_json(
|
|
{
|
|
"type": "addPeer",
|
|
"data": {
|
|
"peer_id": session.id,
|
|
"peer_name": session.name,
|
|
"has_media": session.has_media,
|
|
"should_create_offer": False,
|
|
},
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"Failed to send addPeer to {peer_session.getName()}: {e}"
|
|
)
|
|
|
|
# Notify new session about existing peer (they should create offer)
|
|
logger.info(
|
|
f"{session.getName()} -> {session.getName()}:addPeer("
|
|
f"{peer_session.getName()}, {lobby.getName()}, should_create_offer=True, "
|
|
f"has_media={peer_session.has_media})"
|
|
)
|
|
try:
|
|
if session.ws:
|
|
await session.ws.send_json(
|
|
{
|
|
"type": "addPeer",
|
|
"data": {
|
|
"peer_id": peer_session.id,
|
|
"peer_name": peer_session.name,
|
|
"has_media": peer_session.has_media,
|
|
"should_create_offer": True,
|
|
},
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to send addPeer to {session.getName()}: {e}")
|
|
else:
|
|
logger.info(
|
|
f"{session.getName()} - Skipping WebRTC connection with "
|
|
f"{peer_session.getName()} (neither has media: "
|
|
f"self={session.has_media}, peer={peer_session.has_media})"
|
|
)
|
|
|
|
@staticmethod
|
|
@with_webrtc_error_handling
|
|
async def handle_remove_peer(
|
|
session: "Session",
|
|
peer_session: "Session",
|
|
lobby: "Lobby"
|
|
) -> None:
|
|
"""
|
|
Handle removing WebRTC peer connections between two sessions.
|
|
|
|
Args:
|
|
session: The session leaving the lobby
|
|
peer_session: The peer session to disconnect from
|
|
lobby: The lobby context
|
|
"""
|
|
# Notify peer about session removal
|
|
if peer_session.ws:
|
|
logger.info(
|
|
f"{peer_session.getName()} <- remove_peer({session.getName()})"
|
|
)
|
|
try:
|
|
await peer_session.ws.send_json({
|
|
"type": "removePeer",
|
|
"data": {"peer_name": session.name, "peer_id": session.id},
|
|
})
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"Failed to send removePeer to {peer_session.getName()}: {e}"
|
|
)
|
|
else:
|
|
logger.warning(
|
|
f"{session.getName()} <- part({lobby.getName()}) - "
|
|
f"No WebSocket connection for {peer_session.getName()}. Skipping."
|
|
)
|
|
|
|
# Remove from peer's lobby_peers
|
|
with peer_session.session_lock:
|
|
if (lobby.id in peer_session.lobby_peers and
|
|
session.id in peer_session.lobby_peers[lobby.id]):
|
|
peer_session.lobby_peers[lobby.id].remove(session.id)
|
|
|
|
# Notify session about peer removal
|
|
if session.ws:
|
|
logger.info(
|
|
f"{session.getName()} <- remove_peer({peer_session.getName()})"
|
|
)
|
|
try:
|
|
await session.ws.send_json({
|
|
"type": "removePeer",
|
|
"data": {
|
|
"peer_name": peer_session.name,
|
|
"peer_id": peer_session.id,
|
|
},
|
|
})
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"Failed to send removePeer to {session.getName()}: {e}"
|
|
)
|
|
else:
|
|
logger.error(
|
|
f"{session.getName()} <- part({lobby.getName()}) - No WebSocket connection."
|
|
)
|