fix: decouple voice intent from server enforcement to prevent involuntary unmute

When a moderator lifted a server mute/deafen, the client was involuntarily
turning on the user's microphone because isMuted/isDeafened conflated user
intent with server enforcement. Now intent (isMuted/isDeafened) is never
mutated by server events. Effective state (intent || serverEnforcement) is
computed at broadcast and hardware time via centralized helpers.
This commit is contained in:
Jannis Braun
2026-03-09 22:25:56 +01:00
parent db1909d785
commit 907f8285cd
9 changed files with 164 additions and 233 deletions
@@ -1,5 +1,6 @@
import React from 'react';
import { useVoiceStore } from '../../stores/voiceStore';
import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../../stores/spaceStore';
import { useAudioTrackPlayer } from '../../hooks/useAudioTrackPlayer';
import type { ParticipantInfo } from '../../hooks/useLiveKit';
@@ -66,7 +67,9 @@ function AudioTrackElement({
*/
export function GlobalAudioRenderer() {
const participants = useVoiceStore((s) => s.participants);
const isDeafened = useVoiceStore((s) => s.isDeafened);
const isDeafenedIntent = useVoiceStore((s) => s.isDeafened);
const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds);
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
const outputVolume = useVoiceStore((s) => s.outputVolume);
const participantVolumes = useVoiceStore((s) => s.participantVolumes);
const streamVolumes = useVoiceStore((s) => s.streamVolumes);
@@ -76,6 +79,12 @@ export function GlobalAudioRenderer() {
const streamAttenuationStrength = useVoiceStore((s) => s.streamAttenuationStrength);
const speakingParticipantIds = useVoiceStore((s) => s.speakingParticipantIds);
// Compute effective deafened: user intent || server enforcement
const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null);
const myOriginId = currentVoiceChannelId ? getMyUserIdForOrigin(getChannelOrigin(currentVoiceChannelId)) : undefined;
const serverKey = (spaceId && myOriginId) ? `${spaceId}:${myOriginId}` : '';
const isDeafened = isDeafenedIntent || serverDeafenedUserIds.has(serverKey);
// Determine if someone is currently speaking (for stream attenuation)
const someoneIsSpeaking = participants.some((p) => !p.isLocal && speakingParticipantIds.has(p.identity));