Fix room switching and audio consistency issues
This commit is contained in:
@@ -75,6 +75,9 @@ export function AppLayout() {
|
|||||||
connectDm: connectDmVoice,
|
connectDm: connectDmVoice,
|
||||||
disconnect: disconnectVoice,
|
disconnect: disconnectVoice,
|
||||||
participants: voiceParticipants,
|
participants: voiceParticipants,
|
||||||
|
isConnected: isVoiceConnected,
|
||||||
|
isConnecting: isVoiceConnecting,
|
||||||
|
connectedChannelId,
|
||||||
} = useLiveKit();
|
} = useLiveKit();
|
||||||
|
|
||||||
// Initialize WebSocket
|
// Initialize WebSocket
|
||||||
@@ -85,24 +88,67 @@ export function AppLayout() {
|
|||||||
setParticipants(voiceParticipants);
|
setParticipants(voiceParticipants);
|
||||||
}, [voiceParticipants, setParticipants]);
|
}, [voiceParticipants, setParticipants]);
|
||||||
|
|
||||||
// Manage voice connection (server voice channels)
|
// Track the last channel we attempted to connect to, to prevent effect loops
|
||||||
useEffect(() => {
|
const lastAttemptedRef = React.useRef<string | null>(null);
|
||||||
if (!isLoading && user && isWsConnected && currentVoiceChannelId) {
|
|
||||||
console.log('[AppLayout] Auto-rejoining voice channel:', currentVoiceChannelId);
|
|
||||||
connectVoice(currentVoiceChannelId);
|
|
||||||
} else if (!isLoading && user && !currentVoiceChannelId && !activeDmCall) {
|
|
||||||
disconnectVoice();
|
|
||||||
}
|
|
||||||
}, [currentVoiceChannelId, connectVoice, disconnectVoice, activeDmCall, isLoading, user, isWsConnected]);
|
|
||||||
|
|
||||||
// Manage DM call connection
|
// Manage voice connection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeDmCall) {
|
if (isLoading || !user || !isWsConnected) return;
|
||||||
connectDmVoice(activeDmCall.dmChannelId);
|
|
||||||
} else if (!currentVoiceChannelId) {
|
const manageConnection = async () => {
|
||||||
disconnectVoice();
|
// Determine what we SHOULD be connected to
|
||||||
}
|
const targetChannelId = activeDmCall
|
||||||
}, [activeDmCall, connectDmVoice, disconnectVoice, currentVoiceChannelId]);
|
? `dm-${activeDmCall.dmChannelId}`
|
||||||
|
: currentVoiceChannelId;
|
||||||
|
|
||||||
|
// 1. If we have a target
|
||||||
|
if (targetChannelId) {
|
||||||
|
// If we're not connected to the RIGHT place, trigger connect.
|
||||||
|
// We IGNORE isVoiceConnecting here to allow "interrupting" a connection
|
||||||
|
// or switching rooms immediately.
|
||||||
|
if (connectedChannelId !== targetChannelId) {
|
||||||
|
// Prevent spamming the same connection attempt if React re-renders
|
||||||
|
if (lastAttemptedRef.current === targetChannelId && isVoiceConnecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[AppLayout] Switching/Connecting to: ${targetChannelId}`);
|
||||||
|
lastAttemptedRef.current = targetChannelId;
|
||||||
|
|
||||||
|
if (activeDmCall) {
|
||||||
|
await connectDmVoice(activeDmCall.dmChannelId);
|
||||||
|
} else {
|
||||||
|
await connectVoice(targetChannelId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We are connected to the right place. Reset ref.
|
||||||
|
lastAttemptedRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. No target — ensure disconnected
|
||||||
|
if (connectedChannelId !== null || isVoiceConnected || isVoiceConnecting) {
|
||||||
|
console.log('[AppLayout] Leaving voice (no target)');
|
||||||
|
lastAttemptedRef.current = null;
|
||||||
|
await disconnectVoice();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
manageConnection();
|
||||||
|
}, [
|
||||||
|
currentVoiceChannelId,
|
||||||
|
activeDmCall,
|
||||||
|
connectedChannelId,
|
||||||
|
isVoiceConnected,
|
||||||
|
isVoiceConnecting,
|
||||||
|
isWsConnected,
|
||||||
|
isLoading,
|
||||||
|
user,
|
||||||
|
connectVoice,
|
||||||
|
connectDmVoice,
|
||||||
|
disconnectVoice
|
||||||
|
]);
|
||||||
|
|
||||||
// Responsive detection
|
// Responsive detection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -117,6 +117,14 @@ export function VoiceUser({ participant, large }: VoiceUserProps) {
|
|||||||
audioEl.play().catch(() => {});
|
audioEl.play().catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (boostSourceRef.current) {
|
||||||
|
boostSourceRef.current.disconnect();
|
||||||
|
boostSourceRef.current = null;
|
||||||
|
boostGainRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [outputVolume, perUserVolume, isDeafened, isLocal, participant.audioTrack]);
|
}, [outputVolume, perUserVolume, isDeafened, isLocal, participant.audioTrack]);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ export function useLiveKit() {
|
|||||||
const [participants, setParticipants] = useState<ParticipantInfo[]>([]);
|
const [participants, setParticipants] = useState<ParticipantInfo[]>([]);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
const [connectionState, setConnectionState] = useState<ConnectionState>(ConnectionState.Disconnected);
|
||||||
|
const [connectedChannelId, setConnectedChannelId] = useState<string | null>(null);
|
||||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||||
const roomRef = useRef<Room | null>(null);
|
const roomRef = useRef<Room | null>(null);
|
||||||
const connectedChannelRef = useRef<string | null>(null);
|
const connectedChannelRef = useRef<string | null>(null);
|
||||||
@@ -212,14 +214,38 @@ export function useLiveKit() {
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [isMuted, isDeafened, inputDeviceId, inputVolume, isConnected, room]);
|
}, [isMuted, isDeafened, inputDeviceId, inputVolume, isConnected]);
|
||||||
|
|
||||||
const connect = useCallback(async (channelId: string) => {
|
const connect = useCallback(async (channelId: string) => {
|
||||||
if (connectedChannelRef.current === channelId && roomRef.current) return;
|
if (connectedChannelRef.current === channelId && roomRef.current?.state === ConnectionState.Connected) return;
|
||||||
const gen = ++_connectGeneration;
|
const gen = ++_connectGeneration;
|
||||||
if (roomRef.current) { try { roomRef.current.disconnect(); } catch { } roomRef.current = null; }
|
|
||||||
|
// 1. Reset state immediately to reflect "Loading/Switching" in UI
|
||||||
|
setRoom(null);
|
||||||
|
setParticipants([]);
|
||||||
|
setIsConnected(false);
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
|
setConnectionState(ConnectionState.Connecting);
|
||||||
|
setConnectionError(null);
|
||||||
|
setConnectedChannelId(null); // Clear this so AppLayout knows we are transitioning
|
||||||
|
|
||||||
useVoiceStore.getState().setIsLiveKitConnected(false);
|
useVoiceStore.getState().setIsLiveKitConnected(false);
|
||||||
|
|
||||||
|
// 2. Strictly disconnect previous room (Local Ref OR Global Ref)
|
||||||
|
// This handles cases where AppLayout might have remounted, losing roomRef but leaving _activeRoom alive.
|
||||||
|
const roomToDisconnect = roomRef.current || _activeRoom;
|
||||||
|
|
||||||
|
if (roomToDisconnect) {
|
||||||
|
try {
|
||||||
|
console.log('[LiveKit] Disconnecting previous room:', roomToDisconnect.name);
|
||||||
|
await roomToDisconnect.disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Error disconnecting from previous room:', err);
|
||||||
|
}
|
||||||
|
roomRef.current = null;
|
||||||
|
_activeRoom = null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { token, url } = await api.livekit.token(channelId);
|
const { token, url } = await api.livekit.token(channelId);
|
||||||
if (gen !== _connectGeneration) return;
|
if (gen !== _connectGeneration) return;
|
||||||
@@ -227,6 +253,7 @@ export function useLiveKit() {
|
|||||||
roomRef.current = newRoom;
|
roomRef.current = newRoom;
|
||||||
|
|
||||||
const guardedUpdate = () => { if (roomRef.current === newRoom) updateParticipants(); };
|
const guardedUpdate = () => { if (roomRef.current === newRoom) updateParticipants(); };
|
||||||
|
// ... existing event listeners ...
|
||||||
newRoom.on(RoomEvent.ParticipantConnected, (participant) => {
|
newRoom.on(RoomEvent.ParticipantConnected, (participant) => {
|
||||||
guardedUpdate();
|
guardedUpdate();
|
||||||
if (useVoiceStore.getState().isDeafened) {
|
if (useVoiceStore.getState().isDeafened) {
|
||||||
@@ -249,20 +276,35 @@ export function useLiveKit() {
|
|||||||
newRoom.on(RoomEvent.DataReceived, handleDataReceived);
|
newRoom.on(RoomEvent.DataReceived, handleDataReceived);
|
||||||
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
|
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
|
||||||
if (roomRef.current === newRoom) {
|
if (roomRef.current === newRoom) {
|
||||||
|
setConnectionState(state);
|
||||||
const connected = state === ConnectionState.Connected;
|
const connected = state === ConnectionState.Connected;
|
||||||
|
const connecting = state === ConnectionState.Connecting || state === ConnectionState.Reconnecting;
|
||||||
|
|
||||||
setIsConnected(connected);
|
setIsConnected(connected);
|
||||||
|
setIsConnecting(connecting);
|
||||||
|
|
||||||
useVoiceStore.getState().setIsLiveKitConnected(connected);
|
useVoiceStore.getState().setIsLiveKitConnected(connected);
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
updateParticipants();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
newRoom.on(RoomEvent.Disconnected, () => {
|
newRoom.on(RoomEvent.Disconnected, () => {
|
||||||
if (roomRef.current !== newRoom) return;
|
if (roomRef.current !== newRoom) return;
|
||||||
|
setConnectionState(ConnectionState.Disconnected);
|
||||||
|
setConnectedChannelId(null);
|
||||||
roomRef.current = null; _activeRoom = null; setIsConnected(false); setRoom(null); setParticipants([]);
|
roomRef.current = null; _activeRoom = null; setIsConnected(false); setRoom(null); setParticipants([]);
|
||||||
useVoiceStore.getState().setIsLiveKitConnected(false);
|
useVoiceStore.getState().setIsLiveKitConnected(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
await newRoom.connect(url, token);
|
await newRoom.connect(url, token);
|
||||||
if (gen !== _connectGeneration) { newRoom.disconnect(); return; }
|
if (gen !== _connectGeneration) { newRoom.disconnect(); return; }
|
||||||
_activeRoom = newRoom; connectedChannelRef.current = channelId; setRoom(newRoom); setIsConnected(true);
|
_activeRoom = newRoom;
|
||||||
|
connectedChannelRef.current = channelId;
|
||||||
|
setConnectedChannelId(channelId);
|
||||||
|
setRoom(newRoom);
|
||||||
|
setIsConnected(true);
|
||||||
useVoiceStore.getState().setIsLiveKitConnected(true);
|
useVoiceStore.getState().setIsLiveKitConnected(true);
|
||||||
|
|
||||||
updateParticipants();
|
updateParticipants();
|
||||||
@@ -271,8 +313,6 @@ export function useLiveKit() {
|
|||||||
const { isMuted: wasMuted, isDeafened: wasDeafened } = useVoiceStore.getState();
|
const { isMuted: wasMuted, isDeafened: wasDeafened } = useVoiceStore.getState();
|
||||||
useVoiceStore.setState({ isCameraOn: false, isScreenSharing: false });
|
useVoiceStore.setState({ isCameraOn: false, isScreenSharing: false });
|
||||||
|
|
||||||
// Mic handling is now done by useEffect
|
|
||||||
|
|
||||||
if (wasDeafened) {
|
if (wasDeafened) {
|
||||||
newRoom.remoteParticipants.forEach((p) => p.setVolume(0));
|
newRoom.remoteParticipants.forEach((p) => p.setVolume(0));
|
||||||
}
|
}
|
||||||
@@ -284,8 +324,30 @@ export function useLiveKit() {
|
|||||||
|
|
||||||
const connectDm = useCallback(async (dmChannelId: string) => {
|
const connectDm = useCallback(async (dmChannelId: string) => {
|
||||||
const gen = ++_connectGeneration;
|
const gen = ++_connectGeneration;
|
||||||
if (roomRef.current) { try { roomRef.current.disconnect(); } catch { } roomRef.current = null; }
|
|
||||||
|
// 1. Reset state immediately
|
||||||
|
setRoom(null);
|
||||||
|
setParticipants([]);
|
||||||
|
setIsConnected(false);
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
|
setConnectionState(ConnectionState.Connecting);
|
||||||
|
setConnectionError(null);
|
||||||
|
setConnectedChannelId(null);
|
||||||
|
|
||||||
|
// 2. Strictly disconnect previous room (Local Ref OR Global Ref)
|
||||||
|
const roomToDisconnect = roomRef.current || _activeRoom;
|
||||||
|
|
||||||
|
if (roomToDisconnect) {
|
||||||
|
try {
|
||||||
|
console.log('[LiveKit] Disconnecting previous room (DM):', roomToDisconnect.name);
|
||||||
|
await roomToDisconnect.disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Error disconnecting from previous room:', err);
|
||||||
|
}
|
||||||
|
roomRef.current = null;
|
||||||
|
_activeRoom = null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { token, url } = await api.livekit.dmToken(dmChannelId);
|
const { token, url } = await api.livekit.dmToken(dmChannelId);
|
||||||
if (gen !== _connectGeneration) return;
|
if (gen !== _connectGeneration) return;
|
||||||
@@ -303,21 +365,33 @@ export function useLiveKit() {
|
|||||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
|
||||||
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
|
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
|
||||||
if (roomRef.current === newRoom) {
|
if (roomRef.current === newRoom) {
|
||||||
|
setConnectionState(state);
|
||||||
const connected = state === ConnectionState.Connected;
|
const connected = state === ConnectionState.Connected;
|
||||||
|
const connecting = state === ConnectionState.Connecting || state === ConnectionState.Reconnecting;
|
||||||
|
|
||||||
setIsConnected(connected);
|
setIsConnected(connected);
|
||||||
|
setIsConnecting(connecting);
|
||||||
|
|
||||||
useVoiceStore.getState().setIsLiveKitConnected(connected);
|
useVoiceStore.getState().setIsLiveKitConnected(connected);
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
updateParticipants();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await newRoom.connect(url, token);
|
await newRoom.connect(url, token);
|
||||||
if (gen !== _connectGeneration) { newRoom.disconnect(); return; }
|
if (gen !== _connectGeneration) { newRoom.disconnect(); return; }
|
||||||
_activeRoom = newRoom; connectedChannelRef.current = `dm-${dmChannelId}`; setRoom(newRoom); setIsConnected(true);
|
const fullId = `dm-${dmChannelId}`;
|
||||||
|
_activeRoom = newRoom;
|
||||||
|
connectedChannelRef.current = fullId;
|
||||||
|
setConnectedChannelId(fullId);
|
||||||
|
setRoom(newRoom);
|
||||||
|
setIsConnected(true);
|
||||||
useVoiceStore.getState().setIsLiveKitConnected(true);
|
useVoiceStore.getState().setIsLiveKitConnected(true);
|
||||||
updateParticipants();
|
updateParticipants();
|
||||||
const { isMuted: wasMuted, isDeafened: wasDeafened } = useVoiceStore.getState();
|
const { isMuted: wasMuted, isDeafened: wasDeafened } = useVoiceStore.getState();
|
||||||
useVoiceStore.setState({ isCameraOn: false, isScreenSharing: false });
|
useVoiceStore.setState({ isCameraOn: false, isScreenSharing: false });
|
||||||
|
|
||||||
// Mic handling is now done by useEffect
|
|
||||||
|
|
||||||
if (wasDeafened) {
|
if (wasDeafened) {
|
||||||
newRoom.remoteParticipants.forEach((p) => p.setVolume(0));
|
newRoom.remoteParticipants.forEach((p) => p.setVolume(0));
|
||||||
}
|
}
|
||||||
@@ -328,12 +402,22 @@ export function useLiveKit() {
|
|||||||
|
|
||||||
const disconnect = useCallback(async () => {
|
const disconnect = useCallback(async () => {
|
||||||
_connectGeneration++;
|
_connectGeneration++;
|
||||||
if (roomRef.current) { await roomRef.current.disconnect(); roomRef.current = null; _activeRoom = null; connectedChannelRef.current = null; setRoom(null); setIsConnected(false); setParticipants([]); useVoiceStore.getState().setIsLiveKitConnected(false); }
|
connectedChannelRef.current = null;
|
||||||
|
setConnectedChannelId(null);
|
||||||
|
if (roomRef.current) {
|
||||||
|
await roomRef.current.disconnect();
|
||||||
|
roomRef.current = null;
|
||||||
|
_activeRoom = null;
|
||||||
|
setRoom(null);
|
||||||
|
setIsConnected(false);
|
||||||
|
setIsConnecting(false);
|
||||||
|
setConnectionState(ConnectionState.Disconnected);
|
||||||
|
setParticipants([]);
|
||||||
|
useVoiceStore.getState().setIsLiveKitConnected(false);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleMic = useCallback(async () => {
|
const toggleMic = useCallback(async () => {
|
||||||
// This is now purely a UI helper, actual toggling logic is in the store and useEffect
|
|
||||||
// But we might want to manually trigger resume here just in case
|
|
||||||
await AudioManager.getInstance().resumeContext();
|
await AudioManager.getInstance().resumeContext();
|
||||||
useVoiceStore.getState().toggleMic();
|
useVoiceStore.getState().toggleMic();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -432,5 +516,5 @@ export function useLiveKit() {
|
|||||||
return () => { _connectGeneration++; if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; _activeRoom = null; } };
|
return () => { _connectGeneration++; if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; _activeRoom = null; } };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { room, participants, isConnected, isConnecting, connectionError, connect, connectDm, disconnect, toggleMic, toggleCamera, toggleScreenShare };
|
return { room, participants, isConnected, isConnecting, connectionState, connectedChannelId, connectionError, connect, connectDm, disconnect, toggleMic, toggleCamera, toggleScreenShare };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,10 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setCurrentVoiceChannel: (channelId) => set({ currentVoiceChannelId: channelId }),
|
setCurrentVoiceChannel: (channelId) => set({
|
||||||
|
currentVoiceChannelId: channelId,
|
||||||
|
activeDmCall: null // Clear active DM call when joining a server channel
|
||||||
|
}),
|
||||||
|
|
||||||
setParticipants: (participants) => set({ participants }),
|
setParticipants: (participants) => set({ participants }),
|
||||||
setConnectionError: (error) => set({ connectionError: error }),
|
setConnectionError: (error) => set({ connectionError: error }),
|
||||||
|
|||||||
Reference in New Issue
Block a user