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:
@@ -2,8 +2,8 @@ import { Room, Track } from 'livekit-client';
|
||||
import { useVoiceStore } from '../stores/voiceStore';
|
||||
import type { ScreenShareConfig } from '../stores/voiceStore';
|
||||
import { getStreamingLimits } from '../stores/settingsStore';
|
||||
import { wsSend } from '../hooks/useWebSocket';
|
||||
import { getPublisherPC, getMediaStreamTrack } from './livekitInternals';
|
||||
import { broadcastVoiceStatus } from './voice';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -224,6 +224,5 @@ export async function changeScreenShare(room: Room): Promise<void> {
|
||||
|
||||
export function handleScreenShareUnpublished(): void {
|
||||
useVoiceStore.setState({ isScreenSharing: false });
|
||||
const { isMuted, isDeafened, isCameraOn } = useVoiceStore.getState();
|
||||
wsSend({ type: 'voice_status', isMuted, isDeafened, isCameraOn, isScreenSharing: false });
|
||||
broadcastVoiceStatus();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,65 @@
|
||||
import { useVoiceStore } from '../stores/voiceStore';
|
||||
import { getChannelOrigin, getMyUserIdForOrigin } from '../stores/spaceStore';
|
||||
import { getChannelOrigin, getMyUserIdForOrigin, useSpaceStore } from '../stores/spaceStore';
|
||||
import { wsSend } from '../hooks/useWebSocket';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Effective-state helpers — single source of truth for broadcasts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Compute effective mute/deafen by merging user intent with server enforcement,
|
||||
* then broadcast the effective voice_status over the WebSocket.
|
||||
*
|
||||
* @param overrideOrigin Pass explicitly when called from a WS handler that
|
||||
* knows the origin. Omit to derive from currentVoiceChannelId.
|
||||
*/
|
||||
export function broadcastVoiceStatus(overrideOrigin?: string): void {
|
||||
const vs = useVoiceStore.getState();
|
||||
const { isMuted, isDeafened, isCameraOn, isScreenSharing, currentVoiceChannelId, serverMutedUserIds, serverDeafenedUserIds } = vs;
|
||||
if (!currentVoiceChannelId) return;
|
||||
|
||||
const origin = overrideOrigin ?? getChannelOrigin(currentVoiceChannelId);
|
||||
const myId = getMyUserIdForOrigin(origin);
|
||||
const spaceId = useSpaceStore.getState().channelToSpaceMap.get(currentVoiceChannelId);
|
||||
const serverKey = (spaceId && myId) ? `${spaceId}:${myId}` : '';
|
||||
|
||||
const effectiveMuted = isMuted || serverMutedUserIds.has(serverKey);
|
||||
const effectiveDeafened = isDeafened || serverDeafenedUserIds.has(serverKey);
|
||||
|
||||
wsSend({ type: 'voice_status', isMuted: effectiveMuted, isDeafened: effectiveDeafened, isCameraOn, isScreenSharing }, origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast the effective deafen state to in-room participants via the
|
||||
* LiveKit data channel. Dynamic-imports getActiveRoom to avoid circular deps.
|
||||
*/
|
||||
export function broadcastDeafenViaLiveKit(): void {
|
||||
const vs = useVoiceStore.getState();
|
||||
const { isDeafened, currentVoiceChannelId, serverDeafenedUserIds } = vs;
|
||||
if (!currentVoiceChannelId) return;
|
||||
|
||||
const origin = getChannelOrigin(currentVoiceChannelId);
|
||||
const myId = getMyUserIdForOrigin(origin);
|
||||
const spaceId = useSpaceStore.getState().channelToSpaceMap.get(currentVoiceChannelId);
|
||||
const serverKey = (spaceId && myId) ? `${spaceId}:${myId}` : '';
|
||||
const effectiveDeafened = isDeafened || serverDeafenedUserIds.has(serverKey);
|
||||
|
||||
import('../hooks/useLiveKit').then(({ getActiveRoom }) => {
|
||||
const room = getActiveRoom();
|
||||
if (room) {
|
||||
const encoder = new TextEncoder();
|
||||
room.localParticipant.publishData(
|
||||
encoder.encode(JSON.stringify({ type: 'deafen', deafened: effectiveDeafened })),
|
||||
{ reliable: true }
|
||||
).catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Voice channel join
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Centralized voice channel join that handles cross-instance cleanup.
|
||||
* When switching from a channel on Instance A to one on Instance B,
|
||||
|
||||
Reference in New Issue
Block a user