fix: prevent ghost voice users after server restart

Replace the stale-flag-based voice re-registration in the WS ready
handler with a server-authoritative check. If the server's voiceStates
doesn't include us (server restarted, lost in-memory voiceRooms),
cleanly tear down the LiveKit session via leaveVoice() + disconnect().
If the server still knows about us (WS blip), do nothing.
This commit is contained in:
Jannis Braun
2026-03-22 01:05:30 +01:00
parent f3ff07aa6d
commit 7e7e77b298
+15 -11
View File
@@ -267,20 +267,24 @@ function handleEvent(origin: string, event: ServerEvent): void {
// at broadcast and hardware time. // at broadcast and hardware time.
} }
// Re-register in voice channel after WS reconnect — the server lost // Server-authoritative voice session check: if we had a voice connection
// voice state on restart, so we must tell it we're still connected. // but the server's voiceStates doesn't include us (server restarted and
// GUARD: Only re-register if LiveKit is actually connected. Background // lost in-memory voiceRooms), tear down the stale LiveKit session cleanly.
// tabs can reconnect WS but not LiveKit — sending voice_join without // If the server still knows about us (WS blip, not a restart), do nothing —
// an active media plane would create ghost users in the sidebar. // useLiveKit's ConnectionStateChanged handler will re-register if needed.
{ {
const { currentVoiceChannelId, isLiveKitConnected } = useVoiceStore.getState(); const { currentVoiceChannelId } = useVoiceStore.getState();
if (currentVoiceChannelId && isLiveKitConnected) { if (currentVoiceChannelId) {
const voiceOrigin = getChannelOrigin(currentVoiceChannelId); const voiceOrigin = getChannelOrigin(currentVoiceChannelId);
if (voiceOrigin === origin) { if (voiceOrigin === origin) {
const myId = event.user.id; const serverKnowsUs = event.voiceStates?.[currentVoiceChannelId]?.includes(event.user.id);
if (myId) addVoiceUser(currentVoiceChannelId, myId); if (!serverKnowsUs) {
wsSend({ type: 'voice_join', channelId: currentVoiceChannelId }, origin); // leaveVoice() clears currentVoiceChannelId first to prevent
broadcastVoiceStatus(origin); // AppLayout from auto-reconnecting (disconnect() fires with
// CLIENT_INITIATED which skips handleForceDisconnect).
useVoiceStore.getState().leaveVoice();
getActiveRoom()?.disconnect();
}
} }
} }
} }