fix: broadcast camera & screen share status via WebSocket for sidebar visibility
Camera and LIVE badges in the channel sidebar were only visible to users
who had joined the same LiveKit room. Widen the voice_status WS event
from {isMuted, isDeafened} to {isMuted, isDeafened, isCameraOn, isScreenSharing}
so all server members see camera/screenshare indicators without joining voice.
This commit is contained in:
@@ -397,6 +397,8 @@ function handleVoiceJoin(event: Record<string, unknown>, userId: string): void {
|
|||||||
channelId,
|
channelId,
|
||||||
isMuted: status.isMuted,
|
isMuted: status.isMuted,
|
||||||
isDeafened: status.isDeafened,
|
isDeafened: status.isDeafened,
|
||||||
|
isCameraOn: status.isCameraOn,
|
||||||
|
isScreenSharing: status.isScreenSharing,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -436,6 +438,8 @@ function handleVoiceJoin(event: Record<string, unknown>, userId: string): void {
|
|||||||
channelId,
|
channelId,
|
||||||
isMuted: status.isMuted,
|
isMuted: status.isMuted,
|
||||||
isDeafened: status.isDeafened,
|
isDeafened: status.isDeafened,
|
||||||
|
isCameraOn: status.isCameraOn,
|
||||||
|
isScreenSharing: status.isScreenSharing,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -764,6 +768,8 @@ function handleChannelAck(event: Record<string, unknown>, userId: string): void
|
|||||||
function handleVoiceStatus(event: Record<string, unknown>, userId: string): void {
|
function handleVoiceStatus(event: Record<string, unknown>, userId: string): void {
|
||||||
const isMuted = event.isMuted === true;
|
const isMuted = event.isMuted === true;
|
||||||
const isDeafened = event.isDeafened === true;
|
const isDeafened = event.isDeafened === true;
|
||||||
|
const isCameraOn = event.isCameraOn === true;
|
||||||
|
const isScreenSharing = event.isScreenSharing === true;
|
||||||
|
|
||||||
const channelId = connectionManager.getUserVoiceChannel(userId);
|
const channelId = connectionManager.getUserVoiceChannel(userId);
|
||||||
if (!channelId) return;
|
if (!channelId) return;
|
||||||
@@ -771,7 +777,7 @@ function handleVoiceStatus(event: Record<string, unknown>, userId: string): void
|
|||||||
const serverId = getChannelServerId(channelId);
|
const serverId = getChannelServerId(channelId);
|
||||||
if (!serverId) return;
|
if (!serverId) return;
|
||||||
|
|
||||||
connectionManager.setVoiceUserStatus(userId, isMuted, isDeafened);
|
connectionManager.setVoiceUserStatus(userId, isMuted, isDeafened, isCameraOn, isScreenSharing);
|
||||||
|
|
||||||
connectionManager.sendToServer(serverId, {
|
connectionManager.sendToServer(serverId, {
|
||||||
type: 'voice_status_update',
|
type: 'voice_status_update',
|
||||||
@@ -779,6 +785,8 @@ function handleVoiceStatus(event: Record<string, unknown>, userId: string): void
|
|||||||
channelId,
|
channelId,
|
||||||
isMuted,
|
isMuted,
|
||||||
isDeafened,
|
isDeafened,
|
||||||
|
isCameraOn,
|
||||||
|
isScreenSharing,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ class ConnectionManager {
|
|||||||
private wsToUser: Map<WebSocket, string> = new Map();
|
private wsToUser: Map<WebSocket, string> = new Map();
|
||||||
// dmChannelId → { callerId, startedAt } — active DM calls
|
// dmChannelId → { callerId, startedAt } — active DM calls
|
||||||
private activeCalls: Map<string, { callerId: string; startedAt: number }> = new Map();
|
private activeCalls: Map<string, { callerId: string; startedAt: number }> = new Map();
|
||||||
// userId → { isMuted, isDeafened } — voice user status (mute/deafen state)
|
// userId → { isMuted, isDeafened, isCameraOn, isScreenSharing } — voice user status
|
||||||
private voiceUserStates: Map<string, { isMuted: boolean; isDeafened: boolean }> = new Map();
|
private voiceUserStates: Map<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }> = new Map();
|
||||||
// userId → Timeout
|
// userId → Timeout
|
||||||
private pendingOfflineTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
private pendingOfflineTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
||||||
|
|
||||||
@@ -205,11 +205,11 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Voice user status management
|
// Voice user status management
|
||||||
setVoiceUserStatus(userId: string, isMuted: boolean, isDeafened: boolean): void {
|
setVoiceUserStatus(userId: string, isMuted: boolean, isDeafened: boolean, isCameraOn: boolean, isScreenSharing: boolean): void {
|
||||||
this.voiceUserStates.set(userId, { isMuted, isDeafened });
|
this.voiceUserStates.set(userId, { isMuted, isDeafened, isCameraOn, isScreenSharing });
|
||||||
}
|
}
|
||||||
|
|
||||||
getVoiceUserStatus(userId: string): { isMuted: boolean; isDeafened: boolean } | undefined {
|
getVoiceUserStatus(userId: string): { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean } | undefined {
|
||||||
return this.voiceUserStates.get(userId);
|
return this.voiceUserStates.get(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ class ConnectionManager {
|
|||||||
this.voiceUserStates.delete(userId);
|
this.voiceUserStates.delete(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllVoiceUserStates(): Map<string, { isMuted: boolean; isDeafened: boolean }> {
|
getAllVoiceUserStates(): Map<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }> {
|
||||||
return this.voiceUserStates;
|
return this.voiceUserStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ function buildReadyPayload(userId: string): {
|
|||||||
dmChannels: DmChannel[];
|
dmChannels: DmChannel[];
|
||||||
folders: ServerFolder[];
|
folders: ServerFolder[];
|
||||||
voiceStates: Record<string, string[]>;
|
voiceStates: Record<string, string[]>;
|
||||||
voiceUserStates: Record<string, { isMuted: boolean; isDeafened: boolean }>;
|
voiceUserStates: Record<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }>;
|
||||||
readStates: ReadState[];
|
readStates: ReadState[];
|
||||||
} {
|
} {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
@@ -506,8 +506,8 @@ function buildReadyPayload(userId: string): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build voice user states — tell the client mute/deafen status of voice users
|
// Build voice user states — tell the client mute/deafen/camera/screenshare status of voice users
|
||||||
const voiceUserStates: Record<string, { isMuted: boolean; isDeafened: boolean }> = {};
|
const voiceUserStates: Record<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }> = {};
|
||||||
for (const chId of Object.keys(voiceStates)) {
|
for (const chId of Object.keys(voiceStates)) {
|
||||||
const usersInChannel = voiceStates[chId];
|
const usersInChannel = voiceStates[chId];
|
||||||
if (usersInChannel) {
|
if (usersInChannel) {
|
||||||
|
|||||||
@@ -186,12 +186,12 @@ export type ClientEvent =
|
|||||||
| { type: 'dm_call_accept'; dmChannelId: string }
|
| { type: 'dm_call_accept'; dmChannelId: string }
|
||||||
| { type: 'dm_call_reject'; dmChannelId: string }
|
| { type: 'dm_call_reject'; dmChannelId: string }
|
||||||
| { type: 'dm_call_end'; dmChannelId: string }
|
| { type: 'dm_call_end'; dmChannelId: string }
|
||||||
| { type: 'voice_status'; isMuted: boolean; isDeafened: boolean }
|
| { type: 'voice_status'; isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }
|
||||||
| { type: 'ping' };
|
| { type: 'ping' };
|
||||||
|
|
||||||
// Server → Client Events
|
// Server → Client Events
|
||||||
export type ServerEvent =
|
export type ServerEvent =
|
||||||
| { type: 'ready'; user: User; servers: ServerWithChannelsAndMembers[]; dmChannels: DmChannel[]; folders?: ServerFolder[]; voiceStates?: Record<string, string[]>; voiceUserStates?: Record<string, { isMuted: boolean; isDeafened: boolean }>; readStates?: ReadState[] }
|
| { type: 'ready'; user: User; servers: ServerWithChannelsAndMembers[]; dmChannels: DmChannel[]; folders?: ServerFolder[]; voiceStates?: Record<string, string[]>; voiceUserStates?: Record<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }>; readStates?: ReadState[] }
|
||||||
| { type: 'message_created'; message: MessageWithUser }
|
| { type: 'message_created'; message: MessageWithUser }
|
||||||
| { type: 'message_updated'; message: MessageWithUser }
|
| { type: 'message_updated'; message: MessageWithUser }
|
||||||
| { type: 'message_deleted'; messageId: string; channelId: string }
|
| { type: 'message_deleted'; messageId: string; channelId: string }
|
||||||
@@ -213,7 +213,7 @@ export type ServerEvent =
|
|||||||
| { type: 'dm_call_accepted'; dmChannelId: string }
|
| { type: 'dm_call_accepted'; dmChannelId: string }
|
||||||
| { type: 'dm_call_rejected'; dmChannelId: string }
|
| { type: 'dm_call_rejected'; dmChannelId: string }
|
||||||
| { type: 'dm_call_ended'; dmChannelId: string }
|
| { type: 'dm_call_ended'; dmChannelId: string }
|
||||||
| { type: 'voice_status_update'; userId: string; channelId: string; isMuted: boolean; isDeafened: boolean }
|
| { type: 'voice_status_update'; userId: string; channelId: string; isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }
|
||||||
| { type: 'dm_channel_created'; dmChannel: DmChannel }
|
| { type: 'dm_channel_created'; dmChannel: DmChannel }
|
||||||
| { type: 'dm_channel_closed'; dmChannelId: string }
|
| { type: 'dm_channel_closed'; dmChannelId: string }
|
||||||
| { type: 'friend_removed'; userId: string }
|
| { type: 'friend_removed'; userId: string }
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export function ChannelSidebar() {
|
|||||||
toggleMic();
|
toggleMic();
|
||||||
// Broadcast mute status via WebSocket so non-joined users can see it
|
// Broadcast mute status via WebSocket so non-joined users can see it
|
||||||
const willBeMuted = !isMuted;
|
const willBeMuted = !isMuted;
|
||||||
wsSend({ type: 'voice_status', isMuted: willBeMuted, isDeafened });
|
const { isCameraOn, isScreenSharing } = useVoiceStore.getState();
|
||||||
|
wsSend({ type: 'voice_status', isMuted: willBeMuted, isDeafened, isCameraOn, isScreenSharing });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeafenToggle = async () => {
|
const handleDeafenToggle = async () => {
|
||||||
@@ -47,7 +48,8 @@ export function ChannelSidebar() {
|
|||||||
if (!willDeafen && isMuted) toggleMic();
|
if (!willDeafen && isMuted) toggleMic();
|
||||||
// Broadcast status via WebSocket so non-joined users can see it
|
// Broadcast status via WebSocket so non-joined users can see it
|
||||||
const willBeMuted = willDeafen ? true : false;
|
const willBeMuted = willDeafen ? true : false;
|
||||||
wsSend({ type: 'voice_status', isMuted: willBeMuted, isDeafened: willDeafen });
|
const { isCameraOn, isScreenSharing } = useVoiceStore.getState();
|
||||||
|
wsSend({ type: 'voice_status', isMuted: willBeMuted, isDeafened: willDeafen, isCameraOn, isScreenSharing });
|
||||||
if (room) {
|
if (room) {
|
||||||
try {
|
try {
|
||||||
// Broadcast deafen state to other participants via LiveKit data channel
|
// Broadcast deafen state to other participants via LiveKit data channel
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ export function VoiceChannel({ channelId, channelName, onClick }: VoiceChannelPr
|
|||||||
const isMuted = userId === currentUserId
|
const isMuted = userId === currentUserId
|
||||||
? localIsMuted
|
? localIsMuted
|
||||||
: (participant?.isMuted ?? wsStatus?.isMuted ?? false);
|
: (participant?.isMuted ?? wsStatus?.isMuted ?? false);
|
||||||
const hasCamera = participant?.isCameraOn ?? false;
|
const hasCamera = participant?.isCameraOn ?? wsStatus?.isCameraOn ?? false;
|
||||||
const isScreenSharing = participant?.isScreenSharing ?? false;
|
const isScreenSharing = participant?.isScreenSharing ?? wsStatus?.isScreenSharing ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={userId} className="flex items-center gap-2 px-2 py-0.5 rounded hover:bg-discord-modifier-hover transition-colors">
|
<div key={userId} className="flex items-center gap-2 px-2 py-0.5 rounded hover:bg-discord-modifier-hover transition-colors">
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export function VoiceControlBar() {
|
|||||||
const handleMute = React.useCallback(async () => {
|
const handleMute = React.useCallback(async () => {
|
||||||
toggleMic();
|
toggleMic();
|
||||||
// Broadcast via WebSocket so sidebar shows status without joining
|
// Broadcast via WebSocket so sidebar shows status without joining
|
||||||
wsSend({ type: 'voice_status', isMuted: !isMuted, isDeafened });
|
wsSend({ type: 'voice_status', isMuted: !isMuted, isDeafened, isCameraOn, isScreenSharing });
|
||||||
}, [isMuted, isDeafened, toggleMic]);
|
}, [isMuted, isDeafened, isCameraOn, isScreenSharing, toggleMic]);
|
||||||
|
|
||||||
const handleDeafen = React.useCallback(async () => {
|
const handleDeafen = React.useCallback(async () => {
|
||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
@@ -39,7 +39,7 @@ export function VoiceControlBar() {
|
|||||||
if (willDeafen && !isMuted) toggleMic();
|
if (willDeafen && !isMuted) toggleMic();
|
||||||
if (!willDeafen && isMuted) toggleMic();
|
if (!willDeafen && isMuted) toggleMic();
|
||||||
// Broadcast via WebSocket
|
// Broadcast via WebSocket
|
||||||
wsSend({ type: 'voice_status', isMuted: willDeafen, isDeafened: willDeafen });
|
wsSend({ type: 'voice_status', isMuted: willDeafen, isDeafened: willDeafen, isCameraOn, isScreenSharing });
|
||||||
if (room) {
|
if (room) {
|
||||||
try {
|
try {
|
||||||
// Broadcast deafen state via LiveKit data channel for in-room users
|
// Broadcast deafen state via LiveKit data channel for in-room users
|
||||||
@@ -52,7 +52,7 @@ export function VoiceControlBar() {
|
|||||||
console.error('[VoiceControlBar] Failed to toggle deafen:', err);
|
console.error('[VoiceControlBar] Failed to toggle deafen:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isDeafened, isMuted, toggleDeafen, toggleMic]);
|
}, [isDeafened, isMuted, isCameraOn, isScreenSharing, toggleDeafen, toggleMic]);
|
||||||
|
|
||||||
const handleCamera = async () => {
|
const handleCamera = async () => {
|
||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
@@ -77,6 +77,9 @@ export function VoiceControlBar() {
|
|||||||
await room.localParticipant.setCameraEnabled(false);
|
await room.localParticipant.setCameraEnabled(false);
|
||||||
}
|
}
|
||||||
toggleCamera();
|
toggleCamera();
|
||||||
|
// Broadcast camera state via WebSocket
|
||||||
|
const { isMuted: m, isDeafened: d, isScreenSharing: ss } = useVoiceStore.getState();
|
||||||
|
wsSend({ type: 'voice_status', isMuted: m, isDeafened: d, isCameraOn: willEnable, isScreenSharing: ss });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[VoiceControlBar] Failed to toggle camera:', err);
|
console.error('[VoiceControlBar] Failed to toggle camera:', err);
|
||||||
}
|
}
|
||||||
@@ -87,9 +90,15 @@ export function VoiceControlBar() {
|
|||||||
if (!room) return;
|
if (!room) return;
|
||||||
try {
|
try {
|
||||||
if (!isScreenSharing) {
|
if (!isScreenSharing) {
|
||||||
await startScreenShare(room);
|
const started = await startScreenShare(room);
|
||||||
|
if (started) {
|
||||||
|
const { isMuted: m, isDeafened: d, isCameraOn: c } = useVoiceStore.getState();
|
||||||
|
wsSend({ type: 'voice_status', isMuted: m, isDeafened: d, isCameraOn: c, isScreenSharing: true });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await stopScreenShare(room);
|
await stopScreenShare(room);
|
||||||
|
const { isMuted: m, isDeafened: d, isCameraOn: c } = useVoiceStore.getState();
|
||||||
|
wsSend({ type: 'voice_status', isMuted: m, isDeafened: d, isCameraOn: c, isScreenSharing: false });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[VoiceControlBar] Failed to toggle screen share:', err);
|
console.error('[VoiceControlBar] Failed to toggle screen share:', err);
|
||||||
|
|||||||
@@ -34,8 +34,12 @@ export function VoiceControls() {
|
|||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
try {
|
try {
|
||||||
await room.localParticipant.setCameraEnabled(!isCameraOn);
|
const willEnable = !isCameraOn;
|
||||||
|
await room.localParticipant.setCameraEnabled(willEnable);
|
||||||
toggleCamera();
|
toggleCamera();
|
||||||
|
// Broadcast camera state via WebSocket
|
||||||
|
const { isMuted: m, isDeafened: d, isScreenSharing: ss } = useVoiceStore.getState();
|
||||||
|
wsSend({ type: 'voice_status', isMuted: m, isDeafened: d, isCameraOn: willEnable, isScreenSharing: ss });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[VoiceControls] Failed to toggle camera:', err);
|
console.error('[VoiceControls] Failed to toggle camera:', err);
|
||||||
}
|
}
|
||||||
@@ -46,9 +50,15 @@ export function VoiceControls() {
|
|||||||
if (!room) return;
|
if (!room) return;
|
||||||
try {
|
try {
|
||||||
if (!isScreenSharing) {
|
if (!isScreenSharing) {
|
||||||
await startScreenShare(room);
|
const started = await startScreenShare(room);
|
||||||
|
if (started) {
|
||||||
|
const { isMuted: m, isDeafened: d, isCameraOn: c } = useVoiceStore.getState();
|
||||||
|
wsSend({ type: 'voice_status', isMuted: m, isDeafened: d, isCameraOn: c, isScreenSharing: true });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await stopScreenShare(room);
|
await stopScreenShare(room);
|
||||||
|
const { isMuted: m, isDeafened: d, isCameraOn: c } = useVoiceStore.getState();
|
||||||
|
wsSend({ type: 'voice_status', isMuted: m, isDeafened: d, isCameraOn: c, isScreenSharing: false });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[VoiceControls] Failed to toggle screen share:', err);
|
console.error('[VoiceControls] Failed to toggle screen share:', err);
|
||||||
|
|||||||
@@ -45,20 +45,20 @@ function handleEvent(event: ServerEvent): void {
|
|||||||
setVoiceUsers(channelId, userIds);
|
setVoiceUsers(channelId, userIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Populate voice user statuses (mute/deafen) from server
|
// Populate voice user statuses (mute/deafen/camera/screenshare) from server
|
||||||
if (event.voiceUserStates) {
|
if (event.voiceUserStates) {
|
||||||
for (const [uid, status] of Object.entries(event.voiceUserStates)) {
|
for (const [uid, status] of Object.entries(event.voiceUserStates)) {
|
||||||
setVoiceUserStatus(uid, status.isMuted, status.isDeafened);
|
setVoiceUserStatus(uid, status.isMuted, status.isDeafened, status.isCameraOn, status.isScreenSharing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Re-register in voice channel if we're still connected to LiveKit
|
// Re-register in voice channel if we're still connected to LiveKit
|
||||||
// (WebSocket reconnect causes server to drop our voice tracking)
|
// (WebSocket reconnect causes server to drop our voice tracking)
|
||||||
{
|
{
|
||||||
const { currentVoiceChannelId, isMuted: curMuted, isDeafened: curDeafened } = useVoiceStore.getState();
|
const { currentVoiceChannelId, isMuted: curMuted, isDeafened: curDeafened, isCameraOn: curCamera, isScreenSharing: curScreen } = useVoiceStore.getState();
|
||||||
if (currentVoiceChannelId) {
|
if (currentVoiceChannelId) {
|
||||||
console.log('[WebSocket] Re-syncing voice status on reconnect:', { currentVoiceChannelId, curMuted, curDeafened });
|
console.log('[WebSocket] Re-syncing voice status on reconnect:', { currentVoiceChannelId, curMuted, curDeafened, curCamera, curScreen });
|
||||||
wsSend({ type: 'voice_join', channelId: currentVoiceChannelId });
|
wsSend({ type: 'voice_join', channelId: currentVoiceChannelId });
|
||||||
wsSend({ type: 'voice_status', isMuted: curMuted, isDeafened: curDeafened });
|
wsSend({ type: 'voice_status', isMuted: curMuted, isDeafened: curDeafened, isCameraOn: curCamera, isScreenSharing: curScreen });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -99,7 +99,7 @@ function handleEvent(event: ServerEvent): void {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'voice_status_update':
|
case 'voice_status_update':
|
||||||
setVoiceUserStatus(event.userId, event.isMuted, event.isDeafened);
|
setVoiceUserStatus(event.userId, event.isMuted, event.isDeafened, event.isCameraOn, event.isScreenSharing);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'member_joined':
|
case 'member_joined':
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ interface VoiceState {
|
|||||||
deafenedUserIds: Set<string>;
|
deafenedUserIds: Set<string>;
|
||||||
setUserDeafened: (userId: string, deafened: boolean) => void;
|
setUserDeafened: (userId: string, deafened: boolean) => void;
|
||||||
// WebSocket-based voice user status (visible without joining LiveKit)
|
// WebSocket-based voice user status (visible without joining LiveKit)
|
||||||
voiceUserStates: Map<string, { isMuted: boolean; isDeafened: boolean }>;
|
voiceUserStates: Map<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }>;
|
||||||
setVoiceUserStatus: (userId: string, isMuted: boolean, isDeafened: boolean) => void;
|
setVoiceUserStatus: (userId: string, isMuted: boolean, isDeafened: boolean, isCameraOn: boolean, isScreenSharing: boolean) => void;
|
||||||
clearVoiceUserStatus: (userId: string) => void;
|
clearVoiceUserStatus: (userId: string) => void;
|
||||||
getVoiceUsers: (channelId: string) => string[];
|
getVoiceUsers: (channelId: string) => string[];
|
||||||
clearAllVoiceUsers: () => void;
|
clearAllVoiceUsers: () => void;
|
||||||
@@ -247,10 +247,10 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
voiceUserStates: new Map(),
|
voiceUserStates: new Map(),
|
||||||
setVoiceUserStatus: (userId, isMuted, isDeafened) => {
|
setVoiceUserStatus: (userId, isMuted, isDeafened, isCameraOn, isScreenSharing) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newMap = new Map(state.voiceUserStates);
|
const newMap = new Map(state.voiceUserStates);
|
||||||
newMap.set(userId, { isMuted, isDeafened });
|
newMap.set(userId, { isMuted, isDeafened, isCameraOn, isScreenSharing });
|
||||||
return { voiceUserStates: newMap };
|
return { voiceUserStates: newMap };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Room, Track, VideoPreset } from 'livekit-client';
|
import { Room, Track, VideoPreset } from 'livekit-client';
|
||||||
import { useVoiceStore } from '../stores/voiceStore';
|
import { useVoiceStore } from '../stores/voiceStore';
|
||||||
import { AudioManager } from '../audio/AudioManager';
|
import { AudioManager } from '../audio/AudioManager';
|
||||||
|
import { wsSend } from '../hooks/useWebSocket';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canonical quality presets — single source of truth.
|
* Canonical quality presets — single source of truth.
|
||||||
@@ -172,4 +173,7 @@ export async function changeScreenShare(room: Room): Promise<void> {
|
|||||||
export function handleScreenShareUnpublished(): void {
|
export function handleScreenShareUnpublished(): void {
|
||||||
AudioManager.getInstance().setScreenShareActive(false);
|
AudioManager.getInstance().setScreenShareActive(false);
|
||||||
useVoiceStore.setState({ isScreenSharing: false });
|
useVoiceStore.setState({ isScreenSharing: false });
|
||||||
|
// Broadcast updated state via WebSocket — OS "Stop Sharing" bypasses our UI
|
||||||
|
const { isMuted, isDeafened, isCameraOn } = useVoiceStore.getState();
|
||||||
|
wsSend({ type: 'voice_status', isMuted, isDeafened, isCameraOn, isScreenSharing: false });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user