185 lines
9.3 KiB
TypeScript
185 lines
9.3 KiB
TypeScript
// ~/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<MediaStream | null>(null);
|
|
// Use useRef for an operation lock to prevent re-entrancy
|
|
const operationLockRef = useRef<boolean>(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;
|
|
} |