From 8d9231bb4997b07393498994ed10edf251423feb Mon Sep 17 00:00:00 2001 From: priyanshunitr Date: Tue, 25 Aug 2026 15:55:33 +0530 Subject: [PATCH 1/5] fixed continuous flash screen issue --- frontend/src/Pages/TeamDebateRoom.tsx | 217 ++++++++++++------ frontend/src/components/SpeechTranscripts.tsx | 8 +- 2 files changed, 152 insertions(+), 73 deletions(-) diff --git a/frontend/src/Pages/TeamDebateRoom.tsx b/frontend/src/Pages/TeamDebateRoom.tsx index 8a0d42d8..f918ed57 100644 --- a/frontend/src/Pages/TeamDebateRoom.tsx +++ b/frontend/src/Pages/TeamDebateRoom.tsx @@ -3,7 +3,10 @@ import { useParams } from "react-router-dom"; import { useAtom } from "jotai"; import { userAtom } from "@/state/userAtom"; import { useUser } from "@/hooks/useUser"; -import { getTeamDebate } from "@/services/teamDebateService"; +import { + getTeamDebate, + type TeamDebate, +} from "@/services/teamDebateService"; import { Button } from "@/components/ui/button"; import JudgmentPopup from "@/components/JudgementPopup"; import SpeechTranscripts from "@/components/SpeechTranscripts"; @@ -106,6 +109,12 @@ interface WSMessage { team2Ready?: number; team1MembersCount?: number; team2MembersCount?: number; + team1Name?: string; + team2Name?: string; + team1ReadyStatus?: Record; + team2ReadyStatus?: Record; + countdown?: number; + assignedToTeam?: "Team1" | "Team2"; spectatorCount?: number; spectator?: { connectionId: string; @@ -159,7 +168,7 @@ const TeamDebateRoom: React.FC = () => { }, [user?.id, userFromHook?.id, currentUser?.id, isUserLoading, isAuthenticated]); // Debate state - const [debate, setDebate] = useState(null); + const [debate, setDebate] = useState(null); const [topic, setTopic] = useState(""); const [localRole, setLocalRole] = useState(null); const [peerRole, setPeerRole] = useState(null); @@ -210,12 +219,17 @@ const TeamDebateRoom: React.FC = () => { // Timer state const [timer, setTimer] = useState(0); - const timerRef = useRef(null); + const timerRef = useRef | null>(null); // Speech recognition state const [isListening, setIsListening] = useState(false); const [currentTranscript, setCurrentTranscript] = useState(""); const recognitionRef = useRef(null); + const isListeningRef = useRef(false); + const shouldListenRef = useRef(false); + const recognitionRestartTimeoutRef = useRef | null>(null); const [speechError, setSpeechError] = useState(null); const [speechTranscripts, setSpeechTranscripts] = useState<{ [key: string]: string; @@ -653,16 +667,15 @@ const TeamDebateRoom: React.FC = () => { }, [timer, debatePhase, isMyTurn, speechTranscripts, localRole, debateId]); useEffect(() => { - currentUserIdRef.current = currentUser?.id; - myTeamIdRef.current = myTeamId; - isTeam1Ref.current = isTeam1; - debatePhaseRef.current = debatePhase; -}, [currentUser?.id, myTeamId, isTeam1, debatePhase]); - + currentUserIdRef.current = currentUser?.id ?? null; + myTeamIdRef.current = myTeamId; + isTeam1Ref.current = isTeam1; + debatePhaseRef.current = debatePhase; + }, [currentUser?.id, myTeamId, isTeam1, debatePhase]); // Initialize WebSocket connection - only need token and debateId // User ID will be extracted from token on backend - + useEffect(() => { const token = getAuthToken(); if (!token || !debateId || !hasDeterminedTeam) { @@ -738,7 +751,6 @@ const TeamDebateRoom: React.FC = () => { const amTeam1 = isTeam1Ref.current; const currentMyTeamId = myTeamIdRef.current; const currentUserId = currentUserIdRef.current; - const currentPhase = debatePhaseRef.current; switch (data.type) { case "stateSync": { @@ -785,24 +797,24 @@ const TeamDebateRoom: React.FC = () => { if (data.team2MembersCount !== undefined) setTeam2MembersCount(data.team2MembersCount); // Update team names if provided (for late joiners) - if ((data as any).team1Name) { + if (data.team1Name) { if (amTeam1) { - setMyTeamName((data as any).team1Name); + setMyTeamName(data.team1Name); } else { - setOpponentTeamName((data as any).team1Name); + setOpponentTeamName(data.team1Name); } } - if ((data as any).team2Name) { + if (data.team2Name) { if (amTeam1) { - setOpponentTeamName((data as any).team2Name); + setOpponentTeamName(data.team2Name); } else { - setMyTeamName((data as any).team2Name); + setMyTeamName(data.team2Name); } } // Update individual player ready status (for late joiners) - if ((data as any).team1ReadyStatus) { - const team1Status = (data as any).team1ReadyStatus as Record; + if (data.team1ReadyStatus) { + const team1Status = data.team1ReadyStatus; setPlayerReadyStatus(prev => { const updated = new Map(prev); Object.entries(team1Status).forEach(([userId, ready]) => { @@ -811,8 +823,8 @@ const TeamDebateRoom: React.FC = () => { return updated; }); } - if ((data as any).team2ReadyStatus) { - const team2Status = (data as any).team2ReadyStatus as Record; + if (data.team2ReadyStatus) { + const team2Status = data.team2ReadyStatus; setPlayerReadyStatus(prev => { const updated = new Map(prev); Object.entries(team2Status).forEach(([userId, ready]) => { @@ -835,8 +847,8 @@ const TeamDebateRoom: React.FC = () => { // Update localReady if we have the user's ready status in stateSync if (currentUserId) { - const team1Status = (data as any).team1ReadyStatus as Record | undefined; - const team2Status = (data as any).team2ReadyStatus as Record | undefined; + const team1Status = data.team1ReadyStatus; + const team2Status = data.team2ReadyStatus; if (amTeam1 && team1Status && team1Status[currentUserId] !== undefined) { setLocalReady(team1Status[currentUserId]); } else if (!amTeam1 && team2Status && team2Status[currentUserId] !== undefined) { @@ -914,7 +926,7 @@ const TeamDebateRoom: React.FC = () => { } case "countdownStart": { // Backend is starting countdown - show it to all users - const countdownValue = (data as any).countdown || 3; + const countdownValue = data.countdown || 3; console.log('✓✓✓ COUNTDOWN STARTED FROM BACKEND:', countdownValue); setCountdown(countdownValue); // Hide setup popup when countdown starts @@ -929,12 +941,12 @@ const TeamDebateRoom: React.FC = () => { case "ready": { console.log("=== READY MESSAGE RECEIVED ==="); console.log("Received ready message:", data); - console.log("Current user:", currentUser?.id); + console.log("Current user:", currentUserId); console.log("Message userId:", data.userId); console.log("Message teamId:", data.teamId); - console.log("Message assignedToTeam:", (data as any).assignedToTeam); - console.log("isTeam1:", isTeam1); - console.log("myTeamId:", myTeamId); + console.log("Message assignedToTeam:", data.assignedToTeam); + console.log("isTeam1:", amTeam1); + console.log("myTeamId:", currentMyTeamId); console.log( "Team1Ready:", data.team1Ready, @@ -951,7 +963,7 @@ const TeamDebateRoom: React.FC = () => { // CRITICAL: Verify the ready status is assigned to the correct team const messageTeamId = data.teamId; const expectedTeamId = currentMyTeamId; // Should be the same regardless of isTeam1 - const assignedTeam = (data as any).assignedToTeam; + const assignedTeam = data.assignedToTeam; // Update the ready status for the specific user who clicked if (data.userId === currentUserId && data.ready !== undefined) { @@ -981,9 +993,8 @@ const TeamDebateRoom: React.FC = () => { setTeam2ReadyCount(data.team2Ready); } // CRITICAL: Update member counts from ready message - // Check both direct access and through (data as any) to handle type issues - const team1Count = data.team1MembersCount ?? (data as any).team1MembersCount; - const team2Count = data.team2MembersCount ?? (data as any).team2MembersCount; + const team1Count = data.team1MembersCount; + const team2Count = data.team2MembersCount; if (team1Count !== undefined && team1Count !== null) { console.log(`✓ Updating team1MembersCount to ${team1Count}`); @@ -1001,20 +1012,18 @@ const TeamDebateRoom: React.FC = () => { // Display what we're showing to the user // CRITICAL: Each user should see their own team correctly - // Use (data as any) to access fields that might not be in TypeScript interface - const dataAny = data as any; const myTeamReadyCount = amTeam1 - ? (data.team1Ready ?? dataAny.team1Ready) - : (data.team2Ready ?? dataAny.team2Ready); + ? (data.team1Ready ?? 0) + : (data.team2Ready ?? 0); const myTeamTotal = amTeam1 - ? (data.team1MembersCount ?? dataAny.team1MembersCount) - : (data.team2MembersCount ?? dataAny.team2MembersCount); + ? (data.team1MembersCount ?? 0) + : (data.team2MembersCount ?? 0); const oppReadyCount = amTeam1 - ? (data.team2Ready ?? dataAny.team2Ready) - : (data.team1Ready ?? dataAny.team1Ready); + ? (data.team2Ready ?? 0) + : (data.team1Ready ?? 0); const oppTeamTotal = amTeam1 - ? (data.team2MembersCount ?? dataAny.team2MembersCount) - : (data.team1MembersCount ?? dataAny.team1MembersCount); + ? (data.team2MembersCount ?? 0) + : (data.team1MembersCount ?? 0); console.log(`[Display] isTeam1=${amTeam1}, myTeamName=${myTeamName}`); console.log(`[Display] My Team (${myTeamName}) Ready: ${myTeamReadyCount}/${myTeamTotal}`); @@ -1206,6 +1215,7 @@ const TeamDebateRoom: React.FC = () => { cancelled = true; if (localStreamRef.current) { localStreamRef.current.getTracks().forEach((track) => track.stop()); + localStreamRef.current = null; } if (wsRef.current) { wsRef.current.close(); @@ -1220,17 +1230,23 @@ const TeamDebateRoom: React.FC = () => { flushPendingCandidates, sendSignalMessage, closePeerConnection, - myTeamId, - currentUser?.id, ]); // Initialize Speech Recognition useEffect(() => { if (speechRecognitionDisabled) { + shouldListenRef.current = false; + isListeningRef.current = false; + if (recognitionRestartTimeoutRef.current) { + clearTimeout(recognitionRestartTimeoutRef.current); + recognitionRestartTimeoutRef.current = null; + } recognitionRef.current = null; return; } + let activeRecognition: SpeechRecognition | null = null; + const initializeSpeechRecognition = () => { if ( "SpeechRecognition" in window || @@ -1243,6 +1259,7 @@ const TeamDebateRoom: React.FC = () => { return; } const recognition = new SpeechRecognition(); + activeRecognition = recognition; recognitionRef.current = recognition; recognition.continuous = true; @@ -1250,6 +1267,7 @@ const TeamDebateRoom: React.FC = () => { recognition.lang = "en-US"; recognition.onstart = () => { + isListeningRef.current = true; setIsListening(true); setSpeechError(null); }; @@ -1303,31 +1321,46 @@ const TeamDebateRoom: React.FC = () => { }; recognition.onend = () => { + isListeningRef.current = false; setIsListening(false); if ( - isMyTurn && - debatePhase !== DebatePhase.Setup && - debatePhase !== DebatePhase.Finished - && !speechRecognitionDisabled + !shouldListenRef.current || + recognitionRef.current !== recognition ) { - setTimeout(() => { - if (recognitionRef.current) { - try { - recognitionRef.current.start(); - } catch (error) { - console.error("Error restarting speech recognition:", error); - } - } - }, 100); + return; + } + + if (recognitionRestartTimeoutRef.current) { + clearTimeout(recognitionRestartTimeoutRef.current); } + recognitionRestartTimeoutRef.current = setTimeout(() => { + recognitionRestartTimeoutRef.current = null; + if ( + !shouldListenRef.current || + recognitionRef.current !== recognition || + isListeningRef.current + ) { + return; + } + + try { + isListeningRef.current = true; + recognition.start(); + } catch (error) { + isListeningRef.current = false; + console.error("Error restarting speech recognition:", error); + } + }, 100); }; recognition.onerror = (event: Event) => { + isListeningRef.current = false; setIsListening(false); console.error("Speech recognition error:", event); const errorEvent = event as Event & { error?: string }; if (errorEvent.error === "not-allowed") { + shouldListenRef.current = false; setSpeechRecognitionDisabled(true); setSpeechError( "Speech recognition is blocked. Please grant microphone permission or disable speech-to-text." @@ -1351,18 +1384,42 @@ const TeamDebateRoom: React.FC = () => { initializeSpeechRecognition(); return () => { - if (recognitionRef.current) { - recognitionRef.current.stop(); + shouldListenRef.current = false; + isListeningRef.current = false; + if (recognitionRestartTimeoutRef.current) { + clearTimeout(recognitionRestartTimeoutRef.current); + recognitionRestartTimeoutRef.current = null; + } + if (recognitionRef.current === activeRecognition) { + recognitionRef.current = null; + } + if (activeRecognition) { + activeRecognition.onstart = null; + activeRecognition.onresult = null; + activeRecognition.onend = null; + activeRecognition.onerror = null; + } + try { + activeRecognition?.stop(); + } catch { + // Recognition may already be stopped. } }; - }, [debatePhase, isMyTurn, currentUser?.id, currentUser?.displayName, speechRecognitionDisabled]); + }, [ + debatePhase, + isMyTurn, + currentUser?.id, + currentUser?.displayName, + speechRecognitionDisabled, + ]); // Start/stop speech recognition based on turn const startSpeechRecognition = useCallback(() => { + const recognition = recognitionRef.current; if ( - !recognitionRef.current || + !recognition || speechRecognitionDisabled || - isListening || + isListeningRef.current || debatePhase === DebatePhase.Setup || debatePhase === DebatePhase.Finished ) { @@ -1370,41 +1427,56 @@ const TeamDebateRoom: React.FC = () => { } try { - recognitionRef.current.start(); + isListeningRef.current = true; + recognition.start(); } catch (error) { + isListeningRef.current = false; console.error("Error starting speech recognition:", error); } - }, [isListening, debatePhase, speechRecognitionDisabled]); + }, [debatePhase, speechRecognitionDisabled]); const stopSpeechRecognition = useCallback(() => { - if (recognitionRef.current && isListening) { + const recognition = recognitionRef.current; + if (recognition && isListeningRef.current) { try { - recognitionRef.current.stop(); + isListeningRef.current = false; + recognition.stop(); + setIsListening(false); } catch (error) { console.error("Error stopping speech recognition:", error); } } - }, [isListening]); + }, []); // Auto start/stop speech recognition based on turn useEffect(() => { - if ( + const shouldListen = isMyTurn && debatePhase !== DebatePhase.Setup && - debatePhase !== DebatePhase.Finished - ) { + debatePhase !== DebatePhase.Finished && + !speechRecognitionDisabled; + + shouldListenRef.current = shouldListen; + + if (shouldListen) { startSpeechRecognition(); } else { stopSpeechRecognition(); } return () => { + shouldListenRef.current = false; + if (recognitionRestartTimeoutRef.current) { + clearTimeout(recognitionRestartTimeoutRef.current); + recognitionRestartTimeoutRef.current = null; + } stopSpeechRecognition(); }; }, [ isMyTurn, debatePhase, + speechRecognitionDisabled, startSpeechRecognition, stopSpeechRecognition, ]); @@ -2001,7 +2073,10 @@ const TeamDebateRoom: React.FC = () => { if (el) { if (isCurrentUser) { localVideoRefs.current.set(member.userId, el); - if (localStreamRef.current) { + if ( + localStreamRef.current && + el.srcObject !== localStreamRef.current + ) { el.srcObject = localStreamRef.current; } } else { diff --git a/frontend/src/components/SpeechTranscripts.tsx b/frontend/src/components/SpeechTranscripts.tsx index e2127db0..8c975a5b 100644 --- a/frontend/src/components/SpeechTranscripts.tsx +++ b/frontend/src/components/SpeechTranscripts.tsx @@ -3,11 +3,13 @@ import React from 'react'; interface SpeechTranscriptsProps { transcripts: { [key: string]: string }; currentPhase: string; + liveTranscript?: string; } const SpeechTranscripts: React.FC = ({ transcripts, currentPhase, + liveTranscript, }) => { const phases = [ 'openingFor', @@ -52,6 +54,8 @@ const SpeechTranscripts: React.FC = ({ {phases.map((phase) => { const transcript = transcripts[phase]; const isCurrentPhase = phase === currentPhase; + const displayedTranscript = + transcript || (isCurrentPhase ? liveTranscript : undefined); return (
= ({ )}
- {transcript ? ( + {displayedTranscript ? (
- {transcript} + {displayedTranscript}
) : (
From 735157c91cb5fe388555b94f309952fb98645e0f Mon Sep 17 00:00:00 2001 From: priyanshunitr Date: Tue, 25 Aug 2026 16:09:14 +0530 Subject: [PATCH 2/5] added mic button --- frontend/src/Pages/TeamDebateRoom.tsx | 123 ++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 19 deletions(-) diff --git a/frontend/src/Pages/TeamDebateRoom.tsx b/frontend/src/Pages/TeamDebateRoom.tsx index f918ed57..60848fba 100644 --- a/frontend/src/Pages/TeamDebateRoom.tsx +++ b/frontend/src/Pages/TeamDebateRoom.tsx @@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button"; import JudgmentPopup from "@/components/JudgementPopup"; import SpeechTranscripts from "@/components/SpeechTranscripts"; import { getAuthToken } from "@/utils/auth"; +import { Mic, MicOff } from "lucide-react"; // Define debate phases as an enum (same as OnlineDebateRoom) enum DebatePhase { @@ -215,6 +216,9 @@ const TeamDebateRoom: React.FC = () => { >(new Map()); const [mediaError, setMediaError] = useState(null); const [isCameraOn, setIsCameraOn] = useState(true); + const isCameraOnRef = useRef(true); + const [isMicOn, setIsMicOn] = useState(true); + const isMicOnRef = useRef(true); // Timer state @@ -265,7 +269,7 @@ const TeamDebateRoom: React.FC = () => { ]; const toggleCamera = useCallback(async () => { - const shouldEnable = !isCameraOn; + const shouldEnable = !isCameraOnRef.current; // Acquire a stream if we're turning the camera back on after it was released if (shouldEnable && !localStreamRef.current) { @@ -274,6 +278,12 @@ const TeamDebateRoom: React.FC = () => { video: { width: 1280, height: 720 }, audio: true, }); + stream.getVideoTracks().forEach((track) => { + track.enabled = shouldEnable; + }); + stream.getAudioTracks().forEach((track) => { + track.enabled = isMicOnRef.current; + }); localStreamRef.current = stream; setLocalStream(stream); @@ -300,11 +310,39 @@ const TeamDebateRoom: React.FC = () => { track.enabled = shouldEnable; }); + isCameraOnRef.current = shouldEnable; setIsCameraOn(shouldEnable); if (shouldEnable) { setMediaError(null); } - }, [currentUser?.id, isCameraOn, setIsCameraOn]); + }, [currentUser?.id]); + + const toggleMicrophone = useCallback(() => { + const stream = localStreamRef.current; + if (!stream) { + console.warn("toggleMicrophone called without an active local stream."); + return; + } + + const audioTracks = stream.getAudioTracks(); + if (audioTracks.length === 0) { + setMediaError( + "No microphone is available. Please check your audio device and permissions." + ); + return; + } + + const shouldEnable = !isMicOnRef.current; + audioTracks.forEach((track) => { + track.enabled = shouldEnable; + }); + + isMicOnRef.current = shouldEnable; + setIsMicOn(shouldEnable); + if (shouldEnable) { + setMediaError(null); + } + }, []); @@ -707,6 +745,12 @@ const TeamDebateRoom: React.FC = () => { return; } + stream.getVideoTracks().forEach((track) => { + track.enabled = isCameraOnRef.current; + }); + stream.getAudioTracks().forEach((track) => { + track.enabled = isMicOnRef.current; + }); localStreamRef.current = stream; setLocalStream(stream); @@ -1455,6 +1499,7 @@ const TeamDebateRoom: React.FC = () => { isMyTurn && debatePhase !== DebatePhase.Setup && debatePhase !== DebatePhase.Finished && + isMicOn && !speechRecognitionDisabled; shouldListenRef.current = shouldListen; @@ -1476,6 +1521,7 @@ const TeamDebateRoom: React.FC = () => { }, [ isMyTurn, debatePhase, + isMicOn, speechRecognitionDisabled, startSpeechRecognition, stopSpeechRecognition, @@ -2040,22 +2086,37 @@ const TeamDebateRoom: React.FC = () => { {isCurrentUser && " (You)"} {isCurrentUser && ( - + <> + + + )}
{isCurrentUser && !isCameraOn ? ( @@ -2111,7 +2172,7 @@ const TeamDebateRoom: React.FC = () => { )} - {/* Camera Toggle Button - Only show for current user's team */} + {/* Media controls - Only show for current user's team */} {currentUser && myTeamMembers.some(m => m.userId === currentUser.id) && (
+
)} From 49e8f0fdd1138bf9ca7f8b05f1ca9ed9e6051148 Mon Sep 17 00:00:00 2001 From: priyanshunitr Date: Tue, 25 Aug 2026 16:37:44 +0530 Subject: [PATCH 3/5] 2v2 video fix with optional TURN server setup --- .env.example | 5 ++ backend/websocket/team_websocket.go | 16 ++++- frontend/src/Pages/TeamDebateRoom.tsx | 99 +++++++++++++++++++++++++-- 3 files changed, 112 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 8c878f38..eebb68f9 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,8 @@ GOOGLE_CLIENT_ID=your_google_client_id_here # Frontend Secrets (place in frontend/.env) VITE_GOOGLE_CLIENT_ID=your_google_client_id_here VITE_BASE_URL=http://localhost:1313 + +# Optional WebRTC TURN relay (recommended for reliable calls across restrictive networks) +VITE_WEBRTC_TURN_URL=turn:your-turn-server.example.com:3478 +VITE_WEBRTC_TURN_USERNAME=your_turn_username +VITE_WEBRTC_TURN_CREDENTIAL=your_turn_credential diff --git a/backend/websocket/team_websocket.go b/backend/websocket/team_websocket.go index 56512d23..10f2013b 100644 --- a/backend/websocket/team_websocket.go +++ b/backend/websocket/team_websocket.go @@ -516,6 +516,20 @@ func handleTeamJoin(room *TeamRoom, conn *websocket.Conn, message TeamMessage, c log.Printf("Team WebSocket write error in room %s: %v", roomKey, err) } } + + // Notify existing participants only after the joining client has acquired + // local media and explicitly announced that it is ready for WebRTC offers. + joinPayload := map[string]any{ + "type": "participantJoined", + "userId": client.UserID.Hex(), + "username": client.Username, + "teamId": client.TeamID.Hex(), + } + for _, r := range snapshotTeamRecipients(room, conn) { + if err := r.SafeWriteJSON(joinPayload); err != nil { + log.Printf("Team WebSocket participant join notification error in room %s: %v", roomKey, err) + } + } } // handleTeamChatMessage handles team chat messages @@ -1176,4 +1190,4 @@ func handleTeamLeave(room *TeamRoom, client *TeamClient, roomKey string) { } broadcastAll(room, payload) log.Printf("[handleTeamLeave] User %s left room %s", client.UserID.Hex(), roomKey) -} \ No newline at end of file +} diff --git a/frontend/src/Pages/TeamDebateRoom.tsx b/frontend/src/Pages/TeamDebateRoom.tsx index 60848fba..c4a59670 100644 --- a/frontend/src/Pages/TeamDebateRoom.tsx +++ b/frontend/src/Pages/TeamDebateRoom.tsx @@ -140,6 +140,26 @@ const BASE_URL = (import.meta.env.VITE_BASE_URL as string | undefined)?.replace(/\/$/, "") ?? window.location.origin; +const WEBRTC_ICE_SERVERS: RTCIceServer[] = [ + { urls: "stun:stun.l.google.com:19302" }, +]; + +const turnUrl = import.meta.env.VITE_WEBRTC_TURN_URL as string | undefined; +const turnUsername = import.meta.env.VITE_WEBRTC_TURN_USERNAME as + | string + | undefined; +const turnCredential = import.meta.env.VITE_WEBRTC_TURN_CREDENTIAL as + | string + | undefined; + +if (turnUrl && turnUsername && turnCredential) { + WEBRTC_ICE_SERVERS.push({ + urls: turnUrl, + username: turnUsername, + credential: turnCredential, + }); +} + // Function to extract JSON from response const extractJSON = (response: string): string => { const fenceRegex = /```(?:json)?\s*([\s\S]*?)\s*```/; @@ -201,6 +221,10 @@ const TeamDebateRoom: React.FC = () => { const localVideoRefs = useRef>(new Map()); const remoteVideoRefs = useRef>(new Map()); const localStreamRef = useRef(null); + const reconnectTimeoutsRef = useRef< + Map> + >(new Map()); + const reconnectPeerRef = useRef<(remoteUserId: string) => void>(() => {}); const debateStartedRef = useRef(false); // Track if debate has started to prevent popup reopening const currentUserIdRef = useRef(null); const myTeamIdRef = useRef(null); @@ -373,6 +397,12 @@ const TeamDebateRoom: React.FC = () => { const closePeerConnection = useCallback( (remoteUserId: string) => { + const reconnectTimeout = reconnectTimeoutsRef.current.get(remoteUserId); + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + reconnectTimeoutsRef.current.delete(remoteUserId); + } + const pc = pcRefs.current.get(remoteUserId); if (pc) { try { @@ -410,9 +440,7 @@ const TeamDebateRoom: React.FC = () => { return undefined; } - const pc = new RTCPeerConnection({ - iceServers: [{ urls: "stun:stun.l.google.com:19302" }], - }); + const pc = new RTCPeerConnection({ iceServers: WEBRTC_ICE_SERVERS }); pcRefs.current.set(remoteUserId, pc); @@ -445,8 +473,36 @@ const TeamDebateRoom: React.FC = () => { pc.oniceconnectionstatechange = () => { const state = pc.iceConnectionState; - if (state === "failed" || state === "disconnected" || state === "closed") { - closePeerConnection(remoteUserId); + const reconnectTimeout = reconnectTimeoutsRef.current.get(remoteUserId); + + if (state === "connected" || state === "completed") { + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + reconnectTimeoutsRef.current.delete(remoteUserId); + } + return; + } + + if ( + (state === "failed" || state === "disconnected") && + !reconnectTimeout + ) { + const delay = state === "failed" ? 0 : 5000; + const timeout = setTimeout(() => { + reconnectTimeoutsRef.current.delete(remoteUserId); + + if (pcRefs.current.get(remoteUserId) !== pc) return; + if ( + pc.iceConnectionState !== "failed" && + pc.iceConnectionState !== "disconnected" + ) { + return; + } + + closePeerConnection(remoteUserId); + reconnectPeerRef.current(remoteUserId); + }, delay); + reconnectTimeoutsRef.current.set(remoteUserId, timeout); } }; @@ -522,6 +578,13 @@ const TeamDebateRoom: React.FC = () => { [createPeerConnection, initiateOffer] ); + useEffect(() => { + reconnectPeerRef.current = ensurePeerConnection; + return () => { + reconnectPeerRef.current = () => {}; + }; + }, [ensurePeerConnection]); + useEffect(() => { remoteStreams.forEach((stream, userId) => { attachStreamToVideo(userId, stream); @@ -777,15 +840,19 @@ const TeamDebateRoom: React.FC = () => { const ws = new WebSocket(wsUrl.toString()); wsRef.current = ws; - ws.onopen = () => { + ws.onopen = async () => { if (cancelled) { ws.close(); return; } console.log("Team debate WebSocket connected"); + await ensureMediaStream(); + + if (cancelled || ws.readyState !== WebSocket.OPEN) { + return; + } ws.send(JSON.stringify({ type: "join" })); - ensureMediaStream(); }; ws.onmessage = async (event) => { @@ -1155,6 +1222,18 @@ const TeamDebateRoom: React.FC = () => { } break; } + case "participantJoined": { + if (!data.userId || data.userId === currentUserId) { + break; + } + + // A previous offer may have been attempted while this participant + // was offline. Reset that stale attempt and negotiate again now that + // the backend has confirmed the target is ready for WebRTC offers. + closePeerConnection(data.userId); + ensurePeerConnection(data.userId); + break; + } case "offer": if ( data.targetUserId !== currentUserId || @@ -1265,12 +1344,18 @@ const TeamDebateRoom: React.FC = () => { wsRef.current.close(); wsRef.current = null; } + reconnectTimeoutsRef.current.forEach((timeout) => clearTimeout(timeout)); + reconnectTimeoutsRef.current.clear(); pcRefs.current.forEach((pc) => pc.close()); + pcRefs.current.clear(); + pendingCandidatesRef.current.clear(); + initiatedOffersRef.current.clear(); }; }, [ debateId, hasDeterminedTeam, createPeerConnection, + ensurePeerConnection, flushPendingCandidates, sendSignalMessage, closePeerConnection, From f2a996ff49075cce76da7d95fbfbd33ff11f089e Mon Sep 17 00:00:00 2001 From: priyanshunitr Date: Tue, 25 Aug 2026 16:45:17 +0530 Subject: [PATCH 4/5] WebRTC cleanup warnings --- frontend/src/Pages/TeamDebateRoom.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/Pages/TeamDebateRoom.tsx b/frontend/src/Pages/TeamDebateRoom.tsx index c4a59670..40aeef01 100644 --- a/frontend/src/Pages/TeamDebateRoom.tsx +++ b/frontend/src/Pages/TeamDebateRoom.tsx @@ -795,6 +795,10 @@ const TeamDebateRoom: React.FC = () => { }); let cancelled = false; + const reconnectTimeouts = reconnectTimeoutsRef.current; + const peerConnections = pcRefs.current; + const pendingCandidates = pendingCandidatesRef.current; + const initiatedOffers = initiatedOffersRef.current; const ensureMediaStream = async () => { try { @@ -1344,12 +1348,12 @@ const TeamDebateRoom: React.FC = () => { wsRef.current.close(); wsRef.current = null; } - reconnectTimeoutsRef.current.forEach((timeout) => clearTimeout(timeout)); - reconnectTimeoutsRef.current.clear(); - pcRefs.current.forEach((pc) => pc.close()); - pcRefs.current.clear(); - pendingCandidatesRef.current.clear(); - initiatedOffersRef.current.clear(); + reconnectTimeouts.forEach((timeout) => clearTimeout(timeout)); + reconnectTimeouts.clear(); + peerConnections.forEach((pc) => pc.close()); + peerConnections.clear(); + pendingCandidates.clear(); + initiatedOffers.clear(); }; }, [ debateId, From ce8d95bc1995bb4ec615308d424504bb9e331012 Mon Sep 17 00:00:00 2001 From: priyanshunitr Date: Wed, 26 Aug 2026 01:07:04 +0530 Subject: [PATCH 5/5] mic state fixed --- frontend/src/Pages/TeamDebateRoom.tsx | 162 +++++++++++++++++--------- 1 file changed, 106 insertions(+), 56 deletions(-) diff --git a/frontend/src/Pages/TeamDebateRoom.tsx b/frontend/src/Pages/TeamDebateRoom.tsx index 40aeef01..3e57bfdc 100644 --- a/frontend/src/Pages/TeamDebateRoom.tsx +++ b/frontend/src/Pages/TeamDebateRoom.tsx @@ -29,6 +29,7 @@ enum DebatePhase { // Define debate roles type DebateRole = "for" | "against"; +type MicControlState = "on" | "off" | "disabled"; type JudgmentData = { opening_statement: { @@ -243,6 +244,7 @@ const TeamDebateRoom: React.FC = () => { const isCameraOnRef = useRef(true); const [isMicOn, setIsMicOn] = useState(true); const isMicOnRef = useRef(true); + const isMicAvailableRef = useRef(false); // Timer state @@ -250,7 +252,6 @@ const TeamDebateRoom: React.FC = () => { const timerRef = useRef | null>(null); // Speech recognition state - const [isListening, setIsListening] = useState(false); const [currentTranscript, setCurrentTranscript] = useState(""); const recognitionRef = useRef(null); const isListeningRef = useRef(false); @@ -306,7 +307,8 @@ const TeamDebateRoom: React.FC = () => { track.enabled = shouldEnable; }); stream.getAudioTracks().forEach((track) => { - track.enabled = isMicOnRef.current; + track.enabled = + isMicAvailableRef.current && isMicOnRef.current; }); localStreamRef.current = stream; setLocalStream(stream); @@ -341,35 +343,6 @@ const TeamDebateRoom: React.FC = () => { } }, [currentUser?.id]); - const toggleMicrophone = useCallback(() => { - const stream = localStreamRef.current; - if (!stream) { - console.warn("toggleMicrophone called without an active local stream."); - return; - } - - const audioTracks = stream.getAudioTracks(); - if (audioTracks.length === 0) { - setMediaError( - "No microphone is available. Please check your audio device and permissions." - ); - return; - } - - const shouldEnable = !isMicOnRef.current; - audioTracks.forEach((track) => { - track.enabled = shouldEnable; - }); - - isMicOnRef.current = shouldEnable; - setIsMicOn(shouldEnable); - if (shouldEnable) { - setMediaError(null); - } - }, []); - - - const pendingCandidatesRef = useRef>(new Map()); const initiatedOffersRef = useRef>(new Set()); @@ -646,6 +619,65 @@ const TeamDebateRoom: React.FC = () => { return false; }, [debatePhase, localRole]); + const isMicAvailable = + isMyTurn && + debatePhase !== DebatePhase.Setup && + debatePhase !== DebatePhase.Finished; + const micControlState: MicControlState = !isMicAvailable + ? "disabled" + : isMicOn + ? "on" + : "off"; + const micControlLabel = + micControlState === "disabled" + ? "Mic Disabled" + : micControlState === "on" + ? "Mic On" + : "Mic Off"; + const micControlTitle = + micControlState === "disabled" + ? "Microphone disabled until your turn" + : micControlState === "on" + ? "Turn microphone off" + : "Turn microphone on"; + + useEffect(() => { + isMicAvailableRef.current = isMicAvailable; + const shouldEnableTrack = isMicAvailable && isMicOn; + localStreamRef.current?.getAudioTracks().forEach((track) => { + track.enabled = shouldEnableTrack; + }); + }, [isMicAvailable, isMicOn, localStream]); + + const toggleMicrophone = useCallback(() => { + if (!isMicAvailable) return; + + const stream = localStreamRef.current; + if (!stream) { + console.warn("toggleMicrophone called without an active local stream."); + return; + } + + const audioTracks = stream.getAudioTracks(); + if (audioTracks.length === 0) { + setMediaError( + "No microphone is available. Please check your audio device and permissions." + ); + return; + } + + const shouldEnable = !isMicOnRef.current; + audioTracks.forEach((track) => { + track.enabled = shouldEnable; + }); + + isMicOnRef.current = shouldEnable; + setIsMicOn(shouldEnable); + if (shouldEnable) { + setMediaError(null); + } + }, [isMicAvailable]); + // Fetch debate details - proceed if we have debateId and either user or token useEffect(() => { const fetchDebate = async () => { @@ -816,7 +848,8 @@ const TeamDebateRoom: React.FC = () => { track.enabled = isCameraOnRef.current; }); stream.getAudioTracks().forEach((track) => { - track.enabled = isMicOnRef.current; + track.enabled = + isMicAvailableRef.current && isMicOnRef.current; }); localStreamRef.current = stream; setLocalStream(stream); @@ -1401,7 +1434,6 @@ const TeamDebateRoom: React.FC = () => { recognition.onstart = () => { isListeningRef.current = true; - setIsListening(true); setSpeechError(null); }; @@ -1455,7 +1487,6 @@ const TeamDebateRoom: React.FC = () => { recognition.onend = () => { isListeningRef.current = false; - setIsListening(false); if ( !shouldListenRef.current || recognitionRef.current !== recognition @@ -1488,7 +1519,6 @@ const TeamDebateRoom: React.FC = () => { recognition.onerror = (event: Event) => { isListeningRef.current = false; - setIsListening(false); console.error("Speech recognition error:", event); const errorEvent = event as Event & { error?: string }; @@ -1574,7 +1604,6 @@ const TeamDebateRoom: React.FC = () => { try { isListeningRef.current = false; recognition.stop(); - setIsListening(false); } catch (error) { console.error("Error stopping speech recognition:", error); } @@ -1585,9 +1614,7 @@ const TeamDebateRoom: React.FC = () => { // Auto start/stop speech recognition based on turn useEffect(() => { const shouldListen = - isMyTurn && - debatePhase !== DebatePhase.Setup && - debatePhase !== DebatePhase.Finished && + isMicAvailable && isMicOn && !speechRecognitionDisabled; @@ -1608,8 +1635,7 @@ const TeamDebateRoom: React.FC = () => { stopSpeechRecognition(); }; }, [ - isMyTurn, - debatePhase, + isMicAvailable, isMicOn, speechRecognitionDisabled, startSpeechRecognition, @@ -2195,11 +2221,18 @@ const TeamDebateRoom: React.FC = () => {