diff --git a/packages/web/src/audio/SpeakingDetector.ts b/packages/web/src/audio/SpeakingDetector.ts index 917a4832..2d3800c3 100644 --- a/packages/web/src/audio/SpeakingDetector.ts +++ b/packages/web/src/audio/SpeakingDetector.ts @@ -1,4 +1,5 @@ import { useVoiceStore } from '../stores/voiceStore'; +import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../stores/spaceStore'; import { AudioManager } from './AudioManager'; import type { ParticipantInfo } from '../hooks/useLiveKit'; @@ -168,8 +169,24 @@ export class SpeakingDetector { // --- Local participant --- if (this.localIdentity) { - if (store.isMuted) { - // Muted → never speaking, reset hold counter + // Compute effective muted/deafened state (intent OR server enforcement) + let effectiveMuted = store.isMuted; + let effectiveDeafened = store.isDeafened; + const channelId = store.currentVoiceChannelId; + if (channelId) { + const spaceId = useSpaceStore.getState().channelToSpaceMap.get(channelId); + if (spaceId) { + const myOriginId = getMyUserIdForOrigin(getChannelOrigin(channelId)); + if (myOriginId) { + const serverKey = `${spaceId}:${myOriginId}`; + effectiveMuted = effectiveMuted || store.serverMutedUserIds.has(serverKey); + effectiveDeafened = effectiveDeafened || store.serverDeafenedUserIds.has(serverKey); + } + } + } + + if (effectiveMuted || effectiveDeafened) { + // Muted or deafened → never speaking, reset hold counter this.holdCounters.delete(this.localIdentity); } else { const analyser = AudioManager.getInstance().getAnalyserNode(); diff --git a/packages/web/src/components/layout/ChannelSidebar.tsx b/packages/web/src/components/layout/ChannelSidebar.tsx index 41de6e13..aba0c35b 100644 --- a/packages/web/src/components/layout/ChannelSidebar.tsx +++ b/packages/web/src/components/layout/ChannelSidebar.tsx @@ -43,6 +43,7 @@ export function ChannelSidebar() { const location = useLocation(); const handleMicToggle = async () => { + if (isServerMuted || isServerDeafened) return; const wasDeafened = useVoiceStore.getState().isDeafened; toggleMic(); broadcastVoiceStatus(); @@ -53,6 +54,7 @@ export function ChannelSidebar() { }; const handleDeafenToggle = async () => { + if (isServerDeafened) return; toggleDeafen(); broadcastVoiceStatus(); broadcastDeafenViaLiveKit(); diff --git a/packages/web/src/components/voice/VoiceControlBar.tsx b/packages/web/src/components/voice/VoiceControlBar.tsx index aa952be9..c9492767 100644 --- a/packages/web/src/components/voice/VoiceControlBar.tsx +++ b/packages/web/src/components/voice/VoiceControlBar.tsx @@ -39,6 +39,7 @@ export function VoiceControlBar() { const qualityBtnRef = useRef(null); const handleMute = React.useCallback(async () => { + if (isServerMuted || isServerDeafened) return; const wasDeafened = useVoiceStore.getState().isDeafened; toggleMic(); broadcastVoiceStatus(); @@ -46,13 +47,14 @@ export function VoiceControlBar() { if (wasDeafened && !useVoiceStore.getState().isDeafened) { broadcastDeafenViaLiveKit(); } - }, [toggleMic]); + }, [isServerMuted, isServerDeafened, toggleMic]); const handleDeafen = React.useCallback(async () => { + if (isServerDeafened) return; toggleDeafen(); broadcastVoiceStatus(); broadcastDeafenViaLiveKit(); - }, [toggleDeafen]); + }, [isServerDeafened, toggleDeafen]); const handleCamera = async () => { const room = getActiveRoom();