// ~/components/webrtc-connection-manager.tsx import { useEffect, useRef } from 'react'; // Removed useState import { useActiveVoiceChannelStore } from '~/store/active-voice-channel'; import { useGatewayWebSocketStore } from '~/store/gateway-websocket'; import { useWebRTCStore } from '~/store/webrtc'; // Ensure WebRTCStatus is exported export function WebRTCConnectionManager() { console.log('WebRTC Manager: Mounting component.'); const { serverId: activeServerId, channelId: activeChannelId, isVoiceActive, } = useActiveVoiceChannelStore(state => ({ serverId: state.serverId, channelId: state.channelId, isVoiceActive: state.isVoiceActive, })); const gatewayStatus = useGatewayWebSocketStore((state) => state.status); const { joinVoiceChannel, leaveVoiceChannel, status: webRTCStatus, currentChannelId: rtcCurrentChannelId, } = useWebRTCStore((state) => ({ joinVoiceChannel: state.joinVoiceChannel, leaveVoiceChannel: state.leaveVoiceChannel, status: state.status, currentChannelId: state.currentChannelId, })); // Use useRef for the stream to avoid re-triggering effect on set const mediaStreamRef = useRef(null); // Use useRef for an operation lock to prevent re-entrancy const operationLockRef = useRef(false); useEffect(() => { console.log('WebRTC Manager: Effect triggered', { activeServerId, activeChannelId, isVoiceActive, gatewayStatus, webRTCStatus, rtcCurrentChannelId, operationLock: operationLockRef.current, }); const manageWebRTCConnection = async () => { if (operationLockRef.current) { console.debug('WebRTC Manager: Operation in progress, skipping.'); return; } const isConnectedToSomeChannel = webRTCStatus !== "IDLE" && webRTCStatus !== "DISCONNECTED" && webRTCStatus !== "FAILED"; // --- Condition to JOIN/SWITCH voice --- if (isVoiceActive && activeServerId && activeChannelId && gatewayStatus === 'CONNECTED') { // Condition 1: Not connected at all, and want to join. // Condition 2: Connected to a DIFFERENT channel, and want to switch. const needsToJoinOrSwitch = !isConnectedToSomeChannel || (rtcCurrentChannelId !== activeChannelId); if (needsToJoinOrSwitch) { operationLockRef.current = true; console.log(`WebRTC Manager: Attempting to join/switch to ${activeServerId}/${activeChannelId}. Current RTC status: ${webRTCStatus}, current RTC channel: ${rtcCurrentChannelId}`); // If currently connected to a different channel, leave it first. if (isConnectedToSomeChannel && rtcCurrentChannelId && rtcCurrentChannelId !== activeChannelId) { console.log(`WebRTC Manager: Leaving current channel ${rtcCurrentChannelId} before switching.`); leaveVoiceChannel(); // leaveVoiceChannel will change webRTCStatus, triggering this effect again. // The operationLock will be released when status becomes IDLE/DISCONNECTED. // No 'return' here needed, let the status change from leave drive the next step. // The lock is set, so next iteration won't try to join immediately. // It will fall through to the lock release logic when state becomes IDLE. } else { // Not connected or switching from a null/same channel (should be IDLE then) let streamToUse = mediaStreamRef.current; // Acquire media if we don't have a usable stream if (!streamToUse || streamToUse.getTracks().every(t => t.readyState === 'ended')) { if (streamToUse) { // Clean up old ended stream streamToUse.getTracks().forEach(track => track.stop()); } try { console.log('WebRTC Manager: Acquiring new local media stream...'); streamToUse = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); mediaStreamRef.current = streamToUse; } catch (err) { console.error('WebRTC Manager: Failed to get user media:', err); useWebRTCStore.setState({ status: "FAILED", lastError: 'Failed to get user media.' }); operationLockRef.current = false; // Release lock on failure return; // Stop further processing for this run } } if (streamToUse) { console.log(`WebRTC Manager: Calling joinVoiceChannel for ${activeServerId}/${activeChannelId}`); await joinVoiceChannel(activeServerId, activeChannelId, streamToUse); // Don't release lock here immediately; let status changes from joinVoiceChannel // (e.g., to CONNECTED or FAILED) handle releasing the lock. } else { console.error('WebRTC Manager: No media stream available to join channel.'); useWebRTCStore.setState({ status: "FAILED", lastError: 'Media stream unavailable.' }); operationLockRef.current = false; // Release lock } } } } // --- Condition to LEAVE voice --- else { // Not (isVoiceActive && activeServerId && activeChannelId && gatewayStatus === 'CONNECTED') if (isConnectedToSomeChannel) { operationLockRef.current = true; console.log('WebRTC Manager: Conditions met to leave active voice channel. Leaving...', { isVoiceActive, activeServerId, gatewayStatus, webRTCStatus }); leaveVoiceChannel(); // Lock will be released when status becomes IDLE/DISCONNECTED. } } // --- Manage operation lock based on final WebRTC state for this "cycle" --- // This part is crucial: if an operation was started, the lock is only released // when the WebRTC state settles into a terminal (IDLE, DISCONNECTED, FAILED) or successful (CONNECTED) state. if (operationLockRef.current) { // Only if a lock was acquired in this effect run or previous if ( webRTCStatus === "IDLE" || webRTCStatus === "DISCONNECTED" || webRTCStatus === "FAILED" || (webRTCStatus === "CONNECTED" && rtcCurrentChannelId === activeChannelId && isVoiceActive) // Successfully connected to desired channel ) { // console.debug(`WebRTC Manager: Releasing operation lock. Status: ${webRTCStatus}`); operationLockRef.current = false; } } // --- Release media stream if no longer needed --- // This should happen if WebRTC is inactive AND user doesn't want voice. if ( mediaStreamRef.current && (webRTCStatus === "IDLE" || webRTCStatus === "DISCONNECTED" || webRTCStatus === "FAILED") && !isVoiceActive // Only if voice is explicitly deactivated ) { console.log('WebRTC Manager: Releasing local media stream as WebRTC is inactive and voice is not desired.'); mediaStreamRef.current.getTracks().forEach(track => track.stop()); mediaStreamRef.current = null; } }; manageWebRTCConnection(); }, [ activeServerId, activeChannelId, isVoiceActive, gatewayStatus, webRTCStatus, rtcCurrentChannelId, joinVoiceChannel, // Stable from Zustand leaveVoiceChannel, // Stable from Zustand ]); // Cleanup on component unmount useEffect(() => { return () => { console.log('WebRTC Manager: Unmounting component.'); // Ensure we attempt to leave if connection is active const { status: currentRtcStatus, leaveVoiceChannel: finalLeave } = useWebRTCStore.getState(); if (currentRtcStatus !== "IDLE" && currentRtcStatus !== "DISCONNECTED") { console.log('WebRTC Manager: Unmounting. Leaving active voice channel.'); finalLeave(); } // Stop any tracks held by the ref if (mediaStreamRef.current) { console.log('WebRTC Manager: Unmounting. Stopping tracks from mediaStreamRef.'); mediaStreamRef.current.getTracks().forEach(track => track.stop()); mediaStreamRef.current = null; } }; }, []); // Empty dependency array for unmount cleanup only return null; }