From 450357db79800bf3d76a76d95ac8b1df20bf8318 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Mon, 1 Sep 2025 15:06:17 -0700 Subject: [PATCH] Moved aioice monkeypatch into a separate file for debug usage --- client/src/App.tsx | 82 +++++++++++++++++++-------- client/src/MediaControl.tsx | 62 ++++++++++++++++---- client/src/UserList.tsx | 6 +- voicebot/debug_aioice.py | 110 ++++++++++++++++++++++++++++++++++++ voicebot/main.py | 108 +---------------------------------- 5 files changed, 224 insertions(+), 144 deletions(-) create mode 100644 voicebot/debug_aioice.py diff --git a/client/src/App.tsx b/client/src/App.tsx index 5bd0d01..cc28cee 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, KeyboardEvent } from "react"; +import React, { useState, useEffect, KeyboardEvent, useCallback } from "react"; import { Input, Paper, Typography } from "@mui/material"; import { Session } from "./GlobalContext"; @@ -8,6 +8,7 @@ import { ws_base, base } from "./Common"; import { Box, Button, Tooltip } from "@mui/material"; import { BrowserRouter as Router, Route, Routes, useParams } from "react-router-dom"; import useWebSocket, { ReadyState } from "react-use-websocket"; +import ConnectionStatus from "./ConnectionStatus"; console.log(`AI Voice Chat Build: ${process.env.REACT_APP_AI_VOICECHAT_BUILD}`); @@ -31,17 +32,29 @@ const LobbyView: React.FC = (props: LobbyProps) => { const [editPassword, setEditPassword] = useState(""); const [socketUrl, setSocketUrl] = useState(null); const [creatingLobby, setCreatingLobby] = useState(false); + const [reconnectAttempt, setReconnectAttempt] = useState(0); - const socket = useWebSocket(socketUrl, { - onOpen: () => console.log("app - WebSocket connection opened."), - onClose: () => console.log("app - WebSocket connection closed."), - onError: (event) => console.error("app - WebSocket error observed:", event), - // onMessage: (event) => console.log("WebSocket message received:"), - // shouldReconnect: (closeEvent) => true, // Will attempt to reconnect on all close events. - reconnectInterval: 3000, + const { + sendJsonMessage, + lastJsonMessage, + readyState + } = useWebSocket(socketUrl, { + onOpen: () => { + console.log("app - WebSocket connection opened."); + setReconnectAttempt(0); + }, + onClose: () => { + console.log("app - WebSocket connection closed."); + setReconnectAttempt(prev => prev + 1); + }, + onError: (event: Event) => console.error("app - WebSocket error observed:", event), + shouldReconnect: (closeEvent) => true, // Will attempt to reconnect on all close events + reconnectInterval: 5000, // Retry every 5 seconds + onReconnectStop: (numAttempts) => { + console.log(`Stopped reconnecting after ${numAttempts} attempts`); + }, share: true, }); - const { sendJsonMessage, lastJsonMessage, readyState } = socket; useEffect(() => { if (lobby && session) { @@ -145,7 +158,10 @@ const LobbyView: React.FC = (props: LobbyProps) => { return ( {readyState !== ReadyState.OPEN || !session ? ( -

Connecting to server...

+ ) : ( <> @@ -219,6 +235,7 @@ const LobbyView: React.FC = (props: LobbyProps) => { const App = () => { const [session, setSession] = useState(null); const [error, setError] = useState(null); + const [sessionRetryAttempt, setSessionRetryAttempt] = useState(0); useEffect(() => { if (error) { @@ -233,11 +250,8 @@ const App = () => { console.log(`App - sessionId`, session.id); }, [session]); - useEffect(() => { - if (session) { - return; - } - const getSession = async () => { + const getSession = useCallback(async () => { + try { const res = await fetch(`${base}/api/session`, { method: "GET", cache: "no-cache", @@ -248,26 +262,44 @@ const App = () => { }); if (res.status >= 400) { - const error = `Unable to connect to AI Voice Chat server! Try refreshing your browser in a few seconds.`; - console.error(error); - setError(error); - return; + throw new Error(`HTTP ${res.status}: Unable to connect to AI Voice Chat server`); } + const data = await res.json(); if (data.error) { - console.error(`App - Server error: ${data.error}`); - setError(data.error); - return; + throw new Error(`Server error: ${data.error}`); } + setSession(data); - }; + setSessionRetryAttempt(0); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; + console.error('Failed to get session:', errorMessage); + setError(errorMessage); + + // Schedule retry after 5 seconds + setSessionRetryAttempt(prev => prev + 1); + setTimeout(() => { + getSession(); // Retry + }, 5000); + } + }, []); + useEffect(() => { + if (session) { + return; + } getSession(); - }, [session, setSession]); + }, [session, getSession]); return ( - {!session &&

Connecting to server...

} + {!session && ( + 0 ? ReadyState.CLOSED : ReadyState.CONNECTING} + reconnectAttempt={sessionRetryAttempt} + /> + )} {session && ( diff --git a/client/src/MediaControl.tsx b/client/src/MediaControl.tsx index 20c9a6b..fce6021 100644 --- a/client/src/MediaControl.tsx +++ b/client/src/MediaControl.tsx @@ -11,6 +11,7 @@ import Videocam from "@mui/icons-material/Videocam"; import Box from "@mui/material/Box"; import useWebSocket, { ReadyState } from "react-use-websocket"; import { Session } from "./GlobalContext"; +import WebRTCStatus from "./WebRTCStatus"; const debug = true; // When true, do not send host candidates to the signaling server. Keeps TURN relays preferred. @@ -131,6 +132,8 @@ interface Peer { local: boolean; dead: boolean; connection?: RTCPeerConnection; + connectionState?: string; + isNegotiating?: boolean; } export type { Peer }; @@ -328,8 +331,25 @@ const MediaAgent = (props: MediaAgentProps) => { const initiatedOfferRef = useRef>(new Set()); const pendingIceCandidatesRef = useRef>(new Map()); + // Update peer states when connection state changes + const updatePeerConnectionState = useCallback((peerId: string, connectionState: string, isNegotiating: boolean = false) => { + setPeers(prevPeers => { + const updatedPeers = { ...prevPeers }; + if (updatedPeers[peerId]) { + updatedPeers[peerId] = { + ...updatedPeers[peerId], + connectionState, + isNegotiating + }; + } + return updatedPeers; + }); + }, [setPeers]); + const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(socketUrl, { share: true, + shouldReconnect: (closeEvent) => true, // Auto-reconnect on connection loss + reconnectInterval: 5000, onError: (err) => { console.error(err); }, @@ -475,6 +495,10 @@ const MediaAgent = (props: MediaAgentProps) => { } console.log(`media-agent - addPeer:${peer.peer_name} Handling negotiationneeded for ${peer.peer_name}`); + + // Mark as negotiating + isNegotiatingRef.current.set(peer_id, true); + updatePeerConnectionState(peer_id, connection.connectionState, true); try { makingOfferRef.current.set(peer_id, true); @@ -517,6 +541,10 @@ const MediaAgent = (props: MediaAgentProps) => { connection.connectionState, event ); + + // Update peer connection state + updatePeerConnectionState(peer_id, connection.connectionState); + if (connection.connectionState === "failed") { setTimeout(() => { if (connection.connectionState === "failed") { @@ -681,6 +709,7 @@ const MediaAgent = (props: MediaAgentProps) => { } catch (err) { console.error(`media-agent - addPeer:${peer.peer_name} Failed to create/send offer:`, err); isNegotiatingRef.current.set(peer_id, false); + updatePeerConnectionState(peer_id, connection.connectionState, false); } finally { // Clear the makingOffer flag after we're done makingOfferRef.current.set(peer_id, false); @@ -737,6 +766,7 @@ const MediaAgent = (props: MediaAgentProps) => { try { await pc.setRemoteDescription(desc); isNegotiatingRef.current.set(peer_id, false); // Negotiation complete + updatePeerConnectionState(peer_id, pc.connectionState, false); console.log(`media-agent - sessionDescription:${peer_name} - Remote description set`); // Process any queued ICE candidates @@ -1222,18 +1252,30 @@ const MediaControl: React.FC = ({ isSelf, peer, className }) {isValid ? ( peer.attributes?.srcObject && ( -