Bots now join on demand

This commit is contained in:
James Ketr 2025-09-03 15:51:47 -07:00
parent b916db243b
commit b5614b9d99
10 changed files with 191 additions and 66 deletions

View File

@ -6,28 +6,21 @@
node_modules node_modules
build build
dist dist
__pycache__ **/__pycache__
**/.venv
*.pyc *.pyc
*.pyo *.pyo
*.pyd *.pyd
*.log *.log
*.swp *.swp
*.swo *.swo
.DS_Store
.vscode
.idea
*.sublime-workspace
*.sublime-project
.env .env
.env.* .env.*
dev-keys
*.pem *.pem
*.key *.key
coverage
*.bak *.bak
*.tmp *.tmp
*.local *.local
package-lock.json package-lock.json
yarn.lock yarn.lock
pnpm-lock.yaml pnpm-lock.yaml
*docker-compose.override.yml

View File

@ -172,7 +172,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
{Object.entries(bots).map(([botName, botInfo]) => { {Object.entries(bots).map(([botName, botInfo]) => {
const providerId = providers[botName]; const providerId = providers[botName];
const providerName = getProviderName(providerId); const providerName = getProviderName(providerId);
return ( return (
<ListItem key={botName}> <ListItem key={botName}>
<ListItemText <ListItemText
@ -182,14 +182,10 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
<Typography variant="body2" component="div"> <Typography variant="body2" component="div">
{botInfo.description} {botInfo.description}
</Typography> </Typography>
<Chip <Chip label={providerName} size="small" variant="outlined" sx={{ mt: 0.5 }} />
label={providerName}
size="small"
variant="outlined"
sx={{ mt: 0.5 }}
/>
</Box> </Box>
} }
secondaryTypographyProps={{ component: "div" }}
/> />
</ListItem> </ListItem>
); );
@ -220,6 +216,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
</Typography> </Typography>
</Box> </Box>
} }
secondaryTypographyProps={{ component: "div" }}
/> />
</ListItem> </ListItem>
))} ))}
@ -239,7 +236,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
{error} {error}
</Alert> </Alert>
)} )}
<Box sx={{ mt: 1 }}> <Box sx={{ mt: 1 }}>
<Typography variant="subtitle2" gutterBottom> <Typography variant="subtitle2" gutterBottom>
Select Bot Select Bot
@ -258,15 +255,8 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
}} }}
onClick={() => setSelectedBot(botName)} onClick={() => setSelectedBot(botName)}
> >
<ListItemText <ListItemText primary={botInfo.name} secondary={botInfo.description} />
primary={botInfo.name} <Chip label={getProviderName(providers[botName])} size="small" variant="outlined" />
secondary={botInfo.description}
/>
<Chip
label={getProviderName(providers[botName])}
size="small"
variant="outlined"
/>
</ListItem> </ListItem>
))} ))}
</List> </List>

View File

@ -64,15 +64,14 @@ services:
- PRODUCTION=${PRODUCTION:-false} - PRODUCTION=${PRODUCTION:-false}
- VOICEBOT_MODE=provider - VOICEBOT_MODE=provider
restart: unless-stopped restart: unless-stopped
network_mode: host
volumes: volumes:
- ./cache:/root/.cache:rw - ./cache:/root/.cache:rw
- ./shared:/shared:ro - ./shared:/shared:ro
- ./voicebot:/voicebot:rw - ./voicebot:/voicebot:rw
- ./voicebot/.venv:/voicebot/.venv:rw - ./voicebot/.venv:/voicebot/.venv:rw
# network_mode: host # network_mode: host
# networks: networks:
# - ai-voicebot-net - ai-voicebot-net
networks: networks:

View File

@ -1433,13 +1433,17 @@ async def request_bot_join_lobby(
# Create a session for the bot # Create a session for the bot
bot_session_id = secrets.token_hex(16) bot_session_id = secrets.token_hex(16)
# Create the Session object for the bot
bot_session = Session(bot_session_id)
logger.info(f"Created session for bot: {bot_session.getName()}")
# Determine server URL for the bot to connect back to # Determine server URL for the bot to connect back to
# Use the server's public URL or construct from request # Use the server's public URL or construct from request
server_base_url = os.getenv("PUBLIC_SERVER_URL", "http://localhost:8000") server_base_url = os.getenv("PUBLIC_SERVER_URL", "http://localhost:8000")
if server_base_url.endswith("/"): if server_base_url.endswith("/"):
server_base_url = server_base_url[:-1] server_base_url = server_base_url[:-1]
bot_nick = request.nick or f"{bot_name}-bot" bot_nick = request.nick or f"{bot_name}-bot-{bot_session_id[:8]}"
# Prepare the join request for the bot provider # Prepare the join request for the bot provider
bot_join_payload = BotJoinPayload( bot_join_payload = BotJoinPayload(
@ -1447,7 +1451,7 @@ async def request_bot_join_lobby(
session_id=bot_session_id, session_id=bot_session_id,
nick=bot_nick, nick=bot_nick,
server_url=f"{server_base_url}{public_url}".rstrip("/"), server_url=f"{server_base_url}{public_url}".rstrip("/"),
insecure=False, # Assume secure by default insecure=True, # Accept self-signed certificates in development
) )
try: try:

View File

@ -11,6 +11,8 @@ import importlib
import pkgutil import pkgutil
import sys import sys
import os import os
import time
from contextlib import asynccontextmanager
from typing import Dict, Any from typing import Dict, Any
# Add the parent directory to sys.path to allow absolute imports # Add the parent directory to sys.path to allow absolute imports
@ -24,11 +26,47 @@ from voicebot.models import JoinRequest
from voicebot.webrtc_signaling import WebRTCSignalingClient from voicebot.webrtc_signaling import WebRTCSignalingClient
app = FastAPI(title="voicebot-bot-orchestrator") @asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
logger.info(f"🚀 Voicebot bot orchestrator started successfully at {time.strftime('%Y-%m-%d %H:%M:%S')}")
# Log the discovered bots
bots = discover_bots()
if bots:
logger.info(f"📋 Discovered {len(bots)} bots: {list(bots.keys())}")
else:
logger.info("⚠️ No bots discovered")
# Check for remote server registration
remote_server_url = os.getenv('VOICEBOT_SERVER_URL')
if remote_server_url:
# Attempt to register with remote server
try:
host = os.getenv('HOST', '0.0.0.0')
port = os.getenv('PORT', '8788')
insecure = os.getenv('VOICEBOT_SERVER_INSECURE', 'false').lower() == 'true'
provider_id = await _perform_server_registration(remote_server_url, host, port, insecure)
logger.info(f"🎉 Successfully registered with remote server! Provider ID: {provider_id}")
except Exception as e:
logger.error(f"❌ Failed to register with remote server: {e}")
logger.warning("⚠️ Bot orchestrator will continue running without remote registration")
else:
logger.info(" No VOICEBOT_SERVER_URL provided - running in local-only mode")
yield
# Shutdown
logger.info("🛑 Voicebot bot orchestrator shutting down")
app = FastAPI(title="voicebot-bot-orchestrator", lifespan=lifespan)
# Lightweight in-memory registry of running bot clients # Lightweight in-memory registry of running bot clients
registry: Dict[str, WebRTCSignalingClient] = {} registry: Dict[str, WebRTCSignalingClient] = {}
# Log module import for debugging reloads
logger.info("📦 Bot orchestrator module imported/reloaded")
def discover_bots() -> Dict[str, Dict[str, Any]]: def discover_bots() -> Dict[str, Dict[str, Any]]:
"""Discover bot modules under the voicebot.bots package that expose bot_info. """Discover bot modules under the voicebot.bots package that expose bot_info.
@ -83,6 +121,8 @@ async def bot_join(bot_name: str, req: JoinRequest):
create_tracks = bot.get("create_tracks") create_tracks = bot.get("create_tracks")
logger.info(f"🤖 Bot {bot_name} joining lobby {req.lobby_id} with nick: '{req.nick}'")
# Start the WebRTCSignalingClient in a background asyncio task and register it # Start the WebRTCSignalingClient in a background asyncio task and register it
client = WebRTCSignalingClient( client = WebRTCSignalingClient(
server_url=req.server_url, server_url=req.server_url,
@ -104,8 +144,14 @@ async def bot_join(bot_name: str, req: JoinRequest):
finally: finally:
registry.pop(run_id, None) registry.pop(run_id, None)
loop = asyncio.get_event_loop() def run_client_in_thread():
threading.Thread(target=loop.run_until_complete, args=(run_client(),), daemon=True).start() """Run the client in a new event loop in a separate thread."""
try:
asyncio.run(run_client())
except Exception:
logger.exception("Bot client thread failed for run %s", run_id)
threading.Thread(target=run_client_in_thread, daemon=True).start()
return {"status": "started", "bot": bot_name, "run_id": run_id} return {"status": "started", "bot": bot_name, "run_id": run_id}
@ -141,8 +187,46 @@ def start_bot_api(host: str = "0.0.0.0", port: int = 8788):
uvicorn.run(app, host=host, port=port) uvicorn.run(app, host=host, port=port)
def _construct_voicebot_url(host: str, port: str) -> str:
"""Construct the voicebot URL based on host and port"""
if host == "0.0.0.0":
import socket
try:
hostname = socket.gethostname()
voicebot_url = f"http://{hostname}:{port}"
logger.info(f"🏠 Using hostname-based URL: {voicebot_url}")
except Exception:
voicebot_url = f"http://localhost:{port}"
logger.info(f"🏠 Using localhost URL: {voicebot_url}")
else:
voicebot_url = f"http://{host}:{port}"
logger.info(f"🏠 Using host-based URL: {voicebot_url}")
return voicebot_url
def _perform_server_registration_sync(server_url: str, host: str, port: str, insecure: bool) -> str:
"""Synchronous wrapper for _perform_server_registration"""
return asyncio.run(_perform_server_registration(server_url, host, port, insecure))
async def _perform_server_registration(server_url: str, host: str, port: str, insecure: bool) -> str:
"""Perform server registration with common logic"""
voicebot_url = _construct_voicebot_url(host, port)
logger.info("⏱️ Waiting 2 seconds before attempting remote registration...")
await asyncio.sleep(2) # Give server time to fully start
provider_id = await register_with_server(server_url, voicebot_url, insecure)
logger.info(f"🎉 Successfully registered with remote server! Provider ID: {provider_id}")
return provider_id
async def register_with_server(server_url: str, voicebot_url: str, insecure: bool = False) -> str: async def register_with_server(server_url: str, voicebot_url: str, insecure: bool = False) -> str:
"""Register this voicebot instance as a bot provider with the main server""" """Register this voicebot instance as a bot provider with the main server"""
logger.info(f"🔗 Attempting to register with remote server at {server_url}")
logger.info(f"📍 Registration details - Voicebot URL: {voicebot_url}, Insecure: {insecure}")
try: try:
# Import httpx locally to avoid dependency issues # Import httpx locally to avoid dependency issues
import httpx import httpx
@ -153,6 +237,8 @@ async def register_with_server(server_url: str, voicebot_url: str, insecure: boo
"description": "AI voicebot provider with speech recognition and synthetic media capabilities" "description": "AI voicebot provider with speech recognition and synthetic media capabilities"
} }
logger.info(f"📤 Sending registration payload: {payload}")
# Prepare SSL context if needed # Prepare SSL context if needed
verify = not insecure verify = not insecure
@ -166,14 +252,15 @@ async def register_with_server(server_url: str, voicebot_url: str, insecure: boo
if response.status_code == 200: if response.status_code == 200:
result = response.json() result = response.json()
provider_id = result.get("provider_id") provider_id = result.get("provider_id")
logger.info(f"Successfully registered with server as provider: {provider_id}") logger.info(f"✅ Successfully registered with server as provider: {provider_id}")
logger.info(f"🎯 Remote server can now discover bots at: {voicebot_url}")
return provider_id return provider_id
else: else:
logger.error(f"Failed to register with server: HTTP {response.status_code}: {response.text}") logger.error(f"Failed to register with server: HTTP {response.status_code}: {response.text}")
raise RuntimeError(f"Registration failed: {response.status_code}") raise RuntimeError(f"Registration failed: {response.status_code}")
except Exception as e: except Exception as e:
logger.error(f"Error registering with server: {e}") logger.error(f"💥 Error registering with server: {e}")
raise raise
@ -186,7 +273,6 @@ def start_bot_provider(
): ):
"""Start the bot provider API server and optionally register with main server""" """Start the bot provider API server and optionally register with main server"""
import time import time
import socket
# Start the FastAPI server in a background thread # Start the FastAPI server in a background thread
# Add reload functionality for development # Add reload functionality for development
@ -212,23 +298,19 @@ def start_bot_provider(
# If server_url is provided, register with the main server # If server_url is provided, register with the main server
if server_url: if server_url:
logger.info(f"🔄 Server URL provided - will attempt registration with: {server_url}")
# Give the server a moment to start # Give the server a moment to start
logger.info("⏱️ Waiting 2 seconds for server to fully start...")
time.sleep(2) time.sleep(2)
# Construct the voicebot URL
voicebot_url = f"http://{host}:{port}"
if host == "0.0.0.0":
# Try to get a better hostname
try:
hostname = socket.gethostname()
voicebot_url = f"http://{hostname}:{port}"
except Exception:
voicebot_url = f"http://localhost:{port}"
try: try:
asyncio.run(register_with_server(server_url, voicebot_url, insecure)) provider_id = _perform_server_registration_sync(server_url, host, str(port), insecure)
logger.info(f"🎉 Registration completed successfully! Provider ID: {provider_id}")
except Exception as e: except Exception as e:
logger.error(f"Failed to register with server: {e}") logger.error(f"❌ Failed to register with server: {e}")
logger.warning("⚠️ Bot orchestrator will continue running without remote registration")
else:
logger.info(" No remote server URL provided - running in local-only mode")
# Keep the main thread alive # Keep the main thread alive
try: try:

View File

@ -8,6 +8,7 @@ import asyncio
import os import os
import fcntl import fcntl
import sys import sys
import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Optional from typing import Optional
@ -41,6 +42,9 @@ def create_client_app(args: VoicebotArgs) -> FastAPI:
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
nonlocal client_task, lock_file nonlocal client_task, lock_file
# Startup # Startup
logger.info(f"🚀 Voicebot client app started at {time.strftime('%Y-%m-%d %H:%M:%S')}")
logger.info(f"📡 Client config - Server: {args.server_url}, Lobby: {args.lobby}, Session: {args.session_name}")
# Use a file lock to prevent multiple instances from starting # Use a file lock to prevent multiple instances from starting
lock_file_path = "/tmp/voicebot_client.lock" lock_file_path = "/tmp/voicebot_client.lock"

View File

@ -27,6 +27,8 @@ if [ "$PRODUCTION" != "true" ]; then
if [ "$MODE" = "provider" ]; then if [ "$MODE" = "provider" ]; then
echo "Running as bot provider with auto-reload..." echo "Running as bot provider with auto-reload..."
export VOICEBOT_MODE=provider export VOICEBOT_MODE=provider
export VOICEBOT_SERVER_URL="https://server:8000/ai-voicebot"
export VOICEBOT_SERVER_INSECURE="true"
exec uv run uvicorn main:uvicorn_app \ exec uv run uvicorn main:uvicorn_app \
--host 0.0.0.0 \ --host 0.0.0.0 \
--port 8788 \ --port 8788 \
@ -37,11 +39,11 @@ if [ "$PRODUCTION" != "true" ]; then
else else
echo "Running as client (connecting to lobby)..." echo "Running as client (connecting to lobby)..."
export VOICEBOT_MODE=client export VOICEBOT_MODE=client
export VOICEBOT_SERVER_URL="https://ketrenos.com/ai-voicebot" export VOICEBOT_SERVER_URL="https://server:8000/ai-voicebot"
export VOICEBOT_SERVER_INSECURE="true"
export VOICEBOT_LOBBY="default" export VOICEBOT_LOBBY="default"
export VOICEBOT_SESSION_NAME="Python Voicebot" export VOICEBOT_SESSION_NAME="Python Voicebot"
export VOICEBOT_PASSWORD="v01c3b0t" export VOICEBOT_PASSWORD="v01c3b0t"
export VOICEBOT_INSECURE="true"
exec uv run uvicorn main:uvicorn_app \ exec uv run uvicorn main:uvicorn_app \
--host 0.0.0.0 \ --host 0.0.0.0 \
--port 8789 \ --port 8789 \
@ -55,6 +57,8 @@ else
if [ "$MODE" = "provider" ]; then if [ "$MODE" = "provider" ]; then
echo "Running as bot provider..." echo "Running as bot provider..."
export VOICEBOT_MODE=provider export VOICEBOT_MODE=provider
export VOICEBOT_SERVER_URL="https://server:8000/ai-voicebot"
export VOICEBOT_SERVER_INSECURE="true"
exec uv run uvicorn main:uvicorn_app \ exec uv run uvicorn main:uvicorn_app \
--host 0.0.0.0 \ --host 0.0.0.0 \
--port 8788 \ --port 8788 \
@ -62,11 +66,11 @@ else
else else
echo "Running as client (connecting to lobby)..." echo "Running as client (connecting to lobby)..."
export VOICEBOT_MODE=client export VOICEBOT_MODE=client
export VOICEBOT_SERVER_URL="https://ai-voicebot.ketrenos.com" export VOICEBOT_SERVER_URL="https://server:8000/ai-voicebot"
export VOICEBOT_SERVER_INSECURE="true"
export VOICEBOT_LOBBY="default" export VOICEBOT_LOBBY="default"
export VOICEBOT_SESSION_NAME="Python Voicebot" export VOICEBOT_SESSION_NAME="Python Voicebot"
export VOICEBOT_PASSWORD="v01c3b0t" export VOICEBOT_PASSWORD="v01c3b0t"
export VOICEBOT_INSECURE="false"
exec uv run uvicorn main:uvicorn_app \ exec uv run uvicorn main:uvicorn_app \
--host 0.0.0.0 \ --host 0.0.0.0 \
--port 8789 \ --port 8789 \

View File

@ -13,11 +13,17 @@ import os
# Add the parent directory to sys.path to allow absolute imports # Add the parent directory to sys.path to allow absolute imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Import logger for reload debugging
from logger import logger
from voicebot.models import VoicebotArgs, VoicebotMode from voicebot.models import VoicebotArgs, VoicebotMode
from voicebot.client_main import main_with_args, start_client_with_reload from voicebot.client_main import main_with_args, start_client_with_reload
from voicebot.bot_orchestrator import start_bot_provider from voicebot.bot_orchestrator import start_bot_provider
from voicebot.client_app import get_app from voicebot.client_app import get_app
# Log module import for debugging reloads
logger.info("📦 Main module imported/reloaded")
# Create app instance for uvicorn import # Create app instance for uvicorn import
uvicorn_app = get_app() uvicorn_app = get_app()
@ -27,7 +33,7 @@ async def main():
parser = argparse.ArgumentParser(description="Python WebRTC voicebot client") parser = argparse.ArgumentParser(description="Python WebRTC voicebot client")
parser.add_argument( parser.add_argument(
"--server-url", "--server-url",
default="http://localhost:8000/ai-voicebot", default="https://server:8000/ai-voicebot",
help="AI-Voicebot lobby and signaling server base URL (http:// or https://)", help="AI-Voicebot lobby and signaling server base URL (http:// or https://)",
) )
parser.add_argument( parser.add_argument(

View File

@ -58,13 +58,13 @@ class VoicebotArgs(BaseModel):
host=os.getenv('VOICEBOT_HOST', '0.0.0.0'), host=os.getenv('VOICEBOT_HOST', '0.0.0.0'),
port=int(os.getenv('VOICEBOT_PORT', '8788')), port=int(os.getenv('VOICEBOT_PORT', '8788')),
reload=os.getenv('VOICEBOT_RELOAD', 'false').lower() == 'true', reload=os.getenv('VOICEBOT_RELOAD', 'false').lower() == 'true',
server_url=os.getenv('VOICEBOT_SERVER_URL', 'http://localhost:8000/ai-voicebot'), server_url=os.getenv('VOICEBOT_SERVER_URL', 'https://server:8000/ai-voicebot'),
lobby=os.getenv('VOICEBOT_LOBBY', 'default'), lobby=os.getenv('VOICEBOT_LOBBY', 'default'),
session_name=os.getenv('VOICEBOT_SESSION_NAME', 'Python Bot'), session_name=os.getenv('VOICEBOT_SESSION_NAME', 'Python Bot'),
session_id=os.getenv('VOICEBOT_SESSION_ID', None), session_id=os.getenv('VOICEBOT_SESSION_ID', None),
password=os.getenv('VOICEBOT_PASSWORD', None), password=os.getenv('VOICEBOT_PASSWORD', None),
private=os.getenv('VOICEBOT_PRIVATE', 'false').lower() == 'true', private=os.getenv('VOICEBOT_PRIVATE', 'false').lower() == 'true',
insecure=os.getenv('VOICEBOT_INSECURE', 'false').lower() == 'true', insecure=os.getenv('VOICEBOT_SERVER_INSECURE', 'false').lower() == 'true',
registration_check_interval=float(os.getenv('VOICEBOT_REGISTRATION_CHECK_INTERVAL', '30.0')) registration_check_interval=float(os.getenv('VOICEBOT_REGISTRATION_CHECK_INTERVAL', '30.0'))
) )

View File

@ -58,6 +58,17 @@ from voicebot.models import Peer, MessageData
from voicebot.utils import create_ssl_context, log_network_info from voicebot.utils import create_ssl_context, log_network_info
def _convert_http_to_ws_url(url: str) -> str:
"""Convert HTTP/HTTPS URL to WebSocket URL by replacing scheme."""
if url.startswith("https://"):
return url.replace("https://", "wss://", 1)
elif url.startswith("http://"):
return url.replace("http://", "ws://", 1)
else:
# Assume it's already a WebSocket URL
return url
class WebSocketProtocol(Protocol): class WebSocketProtocol(Protocol):
def send(self, message: object, text: Optional[bool] = None) -> Awaitable[None]: ... def send(self, message: object, text: Optional[bool] = None) -> Awaitable[None]: ...
def close(self, code: int = 1000, reason: str = "") -> Awaitable[None]: ... def close(self, code: int = 1000, reason: str = "") -> Awaitable[None]: ...
@ -93,7 +104,7 @@ class WebRTCSignalingClient:
self.websocket: Optional[object] = None self.websocket: Optional[object] = None
# Optional password to register or takeover a name # Optional password to register or takeover a name
self.name_password: Optional[str] = None self.name_password: Optional[str] = session_name
self.peers: dict[str, Peer] = {} self.peers: dict[str, Peer] = {}
self.peer_connections: dict[str, RTCPeerConnection] = {} self.peer_connections: dict[str, RTCPeerConnection] = {}
@ -120,18 +131,29 @@ class WebRTCSignalingClient:
async def connect(self): async def connect(self):
"""Connect to the signaling server""" """Connect to the signaling server"""
ws_url = f"{self.server_url}/ws/lobby/{self.lobby_id}/{self.session_id}" base_ws_url = _convert_http_to_ws_url(self.server_url)
ws_url = f"{base_ws_url}/ws/lobby/{self.lobby_id}/{self.session_id}"
logger.info(f"Connecting to signaling server: {ws_url}") logger.info(f"Connecting to signaling server: {ws_url}")
# Log network information for debugging # Log network information for debugging
log_network_info() log_network_info()
try: try:
# If insecure (self-signed certs), create an SSL context for the websocket # Create SSL context based on URL scheme and insecure setting
ws_ssl = create_ssl_context(self.insecure) if ws_url.startswith("wss://"):
# For wss://, we need an SSL context
if self.insecure:
# Accept self-signed certificates
ws_ssl = create_ssl_context(insecure=True)
else:
# Use default SSL context for secure connections
ws_ssl = True
else:
# For ws://, no SSL context needed
ws_ssl = None
logger.info( logger.info(
f"Attempting websocket connection to {ws_url} with ssl={bool(ws_ssl)}" f"Attempting websocket connection to {ws_url} with ssl={ws_ssl}"
) )
self.websocket = await websockets.connect(ws_url, ssl=ws_ssl) self.websocket = await websockets.connect(ws_url, ssl=ws_ssl)
logger.info("Connected to signaling server") logger.info("Connected to signaling server")
@ -263,10 +285,21 @@ class WebRTCSignalingClient:
self.websocket = None self.websocket = None
# Reconnect # Reconnect
ws_url = f"{self.server_url}/ws/lobby/{self.lobby_id}/{self.session_id}" base_ws_url = _convert_http_to_ws_url(self.server_url)
ws_url = f"{base_ws_url}/ws/lobby/{self.lobby_id}/{self.session_id}"
# If insecure (self-signed certs), create an SSL context for the websocket # Create SSL context based on URL scheme and insecure setting
ws_ssl = create_ssl_context(self.insecure) if ws_url.startswith("wss://"):
# For wss://, we need an SSL context
if self.insecure:
# Accept self-signed certificates
ws_ssl = create_ssl_context(insecure=True)
else:
# Use default SSL context for secure connections
ws_ssl = True
else:
# For ws://, no SSL context needed
ws_ssl = None
logger.info(f"Reconnecting to signaling server: {ws_url}") logger.info(f"Reconnecting to signaling server: {ws_url}")
self.websocket = await websockets.connect(ws_url, ssl=ws_ssl) self.websocket = await websockets.connect(ws_url, ssl=ws_ssl)
@ -377,7 +410,13 @@ class WebRTCSignalingClient:
async def _process_message(self, message: MessageData): async def _process_message(self, message: MessageData):
"""Process incoming signaling messages""" """Process incoming signaling messages"""
try: try:
# Validate the base message structure first # Handle error messages specially since they have a different structure
if message.get("type") == "error" and "error" in message:
error_msg = message.get("error", "Unknown error")
logger.error(f"Received error from signaling server: {error_msg}")
return
# Validate the base message structure for non-error messages
validated_message = WebSocketMessageModel.model_validate(message) validated_message = WebSocketMessageModel.model_validate(message)
msg_type = validated_message.type msg_type = validated_message.type
data = validated_message.data data = validated_message.data
@ -448,6 +487,10 @@ class WebRTCSignalingClient:
logger.error(f"Invalid update payload: {e}", exc_info=True) logger.error(f"Invalid update payload: {e}", exc_info=True)
return return
logger.info(f"Received update message: {validated}") logger.info(f"Received update message: {validated}")
elif msg_type == "status_check":
# Handle status check messages - these are used to verify connection
logger.debug(f"Received status check message: {data}")
# No special processing needed for status checks, just acknowledge receipt
else: else:
logger.info(f"Unhandled message type: {msg_type} with data: {data}") logger.info(f"Unhandled message type: {msg_type} with data: {data}")