diff --git a/packages/server/src/routes/federation.ts b/packages/server/src/routes/federation.ts index 7eebfcdb..c55a4989 100644 --- a/packages/server/src/routes/federation.ts +++ b/packages/server/src/routes/federation.ts @@ -3561,7 +3561,6 @@ function processDmCallAcceptEvent( accepted: string[], rejected: Array<{ messageId: string; reason: string }>, ): void { - console.log(`[S2S_CALL_ACCEPT] from=${sourceInstance} federatedId=${event.federatedId} acceptor=${JSON.stringify(event.call?.acceptor)}`); if (!event.call?.acceptor || !event.federatedId) { rejected.push({ messageId: event.messageId, reason: 'missing_call_payload' }); return; @@ -3577,11 +3576,9 @@ function processDmCallAcceptEvent( .where(eq(schema.dmChannels.federatedId, event.federatedId)) .get(); const dmChannelId = channel?.id; - console.log(`[S2S_CALL_ACCEPT] channel lookup: federatedId=${event.federatedId} → dmChannelId=${dmChannelId ?? 'NOT FOUND'}`); // Check if we're the HOST (have a VoiceRoom) const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined; - console.log(`[S2S_CALL_ACCEPT] room lookup: ${room ? `found (type=${room.roomType}, state=${(room.metadata as any).state})` : 'NOT FOUND'}`); if (room && room.roomType === 'dm') { const meta = room.metadata as DmRoomMeta; @@ -3600,12 +3597,12 @@ function processDmCallAcceptEvent( }); } - // Broadcast accepted locally - console.log(`[S2S_CALL_ACCEPT] HOST path: broadcasting dm_call_accepted to dmChannelId=${dmChannelId}`); + // Broadcast accepted locally — include federatedCallId so all clients can match connectionManager.sendToDmMembers(dmChannelId!, { type: 'dm_call_accepted', dmChannelId: dmChannelId!, - }); + federatedCallId: event.federatedId, + } as ServerEvent); // Fan out to ALL other remote instances (exclude the one that sent the accept) const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`; diff --git a/packages/server/src/routes/livekit.ts b/packages/server/src/routes/livekit.ts index c5f5948f..c2c6ad64 100644 --- a/packages/server/src/routes/livekit.ts +++ b/packages/server/src/routes/livekit.ts @@ -48,7 +48,6 @@ export async function livekitRoutes(app: FastifyInstance): Promise { } const { channelId, dmChannelId } = request.body as { channelId?: string; dmChannelId?: string }; - console.log(`[LIVEKIT_TOKEN] userId=${request.userId} username=${request.username} channelId=${channelId} dmChannelId=${dmChannelId}`); // Determine room name based on channel type let roomName: string; @@ -119,7 +118,6 @@ export async function livekitRoutes(app: FastifyInstance): Promise { const jwt = await token.toJwt(); const livekitUrl = config.livekit.url ?? ''; - console.log(`[LIVEKIT_TOKEN] ISSUED room=${roomName} identity=${identity} url=${livekitUrl}`); const response: LiveKitTokenResponse = { token: jwt, diff --git a/packages/server/src/ws/events.ts b/packages/server/src/ws/events.ts index 2a98ae62..61c8d6b8 100644 --- a/packages/server/src/ws/events.ts +++ b/packages/server/src/ws/events.ts @@ -1444,8 +1444,6 @@ function handleDmCallStart(event: Record, userId: string, usern function handleDmCallAccept(event: Record, userId: string, ws: WebSocket): void { let dmChannelId = (event.dmChannelId as string) || null; const federatedCallId = (event.federatedCallId as string) || null; - console.log(`[DM_CALL_ACCEPT] userId=${userId} dmChannelId=${dmChannelId} federatedCallId=${federatedCallId}`); - if (!dmChannelId && !federatedCallId) { connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId or federatedCallId is required' }); return; @@ -1462,22 +1460,17 @@ function handleDmCallAccept(event: Record, userId: string, ws: .get(); if (ch) { dmChannelId = ch.id; - console.log(`[DM_CALL_ACCEPT] Resolved federatedId → dmChannelId=${ch.id}`); - } else { - console.log(`[DM_CALL_ACCEPT] No channel found for federatedId=${federatedCallId}`); } } // Path 1: Local room (we're the host) — only possible with dmChannelId if (dmChannelId) { if (!isDmMember(dmChannelId, userId)) { - console.log(`[DM_CALL_ACCEPT] NOT a member of dmChannelId=${dmChannelId}`); connectionManager.sendToUser(userId, { type: 'error', message: 'You are not a member of this DM channel' }); return; } const room = connectionManager.getRoom(dmChannelId); - console.log(`[DM_CALL_ACCEPT] Path1: getRoom(${dmChannelId}) → ${room ? `found (type=${room.roomType}, state=${(room.metadata as any).state})` : 'NOT FOUND'}`); if (room && room.roomType === 'dm') { const meta = room.metadata as DmRoomMeta; @@ -1498,14 +1491,20 @@ function handleDmCallAccept(event: Record, userId: string, ws: if (acceptorLeft) broadcastRoomLeave(acceptorLeft.roomId, acceptorLeft.room, userId); connectionManager.joinRoom(dmChannelId, userId); connectionManager.setVoiceWs(userId, ws); - connectionManager.sendToDmMembers(dmChannelId, { type: 'dm_call_accepted', dmChannelId }); + // Look up federatedId for the broadcast so all clients (including remote) can match + const fedIdRow = getDb().select({ federatedId: schema.dmChannels.federatedId }) + .from(schema.dmChannels).where(eq(schema.dmChannels.id, dmChannelId)).get(); + connectionManager.sendToDmMembers(dmChannelId, { + type: 'dm_call_accepted', + dmChannelId, + federatedCallId: fedIdRow?.federatedId ?? undefined, + } as ServerEvent); connectionManager.sendToDmMembers(dmChannelId, { type: 'voice_state_update', channelId: dmChannelId, userId, action: 'join', }); - console.log(`[DM_CALL_ACCEPT] Path1 HOST SUCCESS: room activated, dm_call_accepted broadcast`); sendFederatedCallAccept(dmChannelId, userId) .catch(err => console.error('[federation] sendFederatedCallAccept error:', err)); return; @@ -1519,7 +1518,6 @@ function handleDmCallAccept(event: Record, userId: string, ws: ? connectionManager.getFederatedCallByDmChannel(dmChannelId) : undefined; - console.log(`[DM_CALL_ACCEPT] Path2: fedCall=${fedCall ? `found (host=${fedCall.federatedCallHost})` : 'NOT FOUND'}`); if (fedCall) { connectionManager.activateFederatedCall(fedCall.federatedId); connectionManager.sendToFederatedCallUsers(fedCall.federatedId, { @@ -1535,7 +1533,6 @@ function handleDmCallAccept(event: Record, userId: string, ws: .get(); const homeUserId = user?.homeUserId || userId; - console.log(`[DM_CALL_ACCEPT] Path2 RELAY to host ${fedCall.federatedCallHost}`); sendCallRelay(fedCall.federatedCallHost, [{ eventType: 'dm_call_accept', messageId: generateSnowflake(), @@ -1545,13 +1542,10 @@ function handleDmCallAccept(event: Record, userId: string, ws: call: { acceptor: { homeUserId, homeInstance: getOurOrigin() }, }, - }]).then(result => { - console.log(`[DM_CALL_ACCEPT] Relay result: ${JSON.stringify(result)}`); - }).catch(err => console.error('[federation] Failed to send dm_call_accept:', err)); + }]).catch(err => console.error('[federation] Failed to send dm_call_accept:', err)); return; } - console.log(`[DM_CALL_ACCEPT] FALLTHROUGH — no call found for userId=${userId}`); connectionManager.sendToUser(userId, { type: 'error', message: 'No active call in this DM channel' }); } diff --git a/packages/web/src/components/voice/SoundController.tsx b/packages/web/src/components/voice/SoundController.tsx index 78611dfa..b94fef5a 100644 --- a/packages/web/src/components/voice/SoundController.tsx +++ b/packages/web/src/components/voice/SoundController.tsx @@ -24,7 +24,9 @@ export function SoundController() { const prevScreenShareUserIds = useRef>(new Set(useVoiceStore.getState().participants.filter(p => p.isScreenSharing).map(p => p.userId))); const incomingCallLoop = useRef(null); + const incomingCallLoading = useRef(false); // sync guard for async playSound const outgoingCallLoop = useRef(null); + const outgoingCallLoading = useRef(false); useEffect(() => { // Set initial mount flag to false after first run @@ -109,24 +111,43 @@ export function SoundController() { prevParticipantIds.current = currentParticipantIds; prevScreenShareUserIds.current = currentScreenShareUserIds; - // Incoming Call (Ringing) - if (state.incomingCall && !incomingCallLoop.current) { + // Incoming Call (Ringing) — sync guard prevents multiple playSound during async load + if (state.incomingCall && !incomingCallLoop.current && !incomingCallLoading.current) { + incomingCallLoading.current = true; audioManager.playSound('call_ringing', { loop: true, volume: getSfxVolume() }).then(source => { - incomingCallLoop.current = source; + // If call was cancelled while sound was loading, stop immediately + if (!useVoiceStore.getState().incomingCall) { + source?.stop(); + } else { + incomingCallLoop.current = source; + } + incomingCallLoading.current = false; }); - } else if (!state.incomingCall && incomingCallLoop.current) { - incomingCallLoop.current.stop(); - incomingCallLoop.current = null; + } else if (!state.incomingCall) { + if (incomingCallLoop.current) { + incomingCallLoop.current.stop(); + incomingCallLoop.current = null; + } + incomingCallLoading.current = false; } - // Outgoing Call (Calling) - if (state.outgoingCall && !outgoingCallLoop.current) { + // Outgoing Call (Calling) — same sync guard pattern + if (state.outgoingCall && !outgoingCallLoop.current && !outgoingCallLoading.current) { + outgoingCallLoading.current = true; audioManager.playSound('call_calling', { loop: true, volume: getSfxVolume() }).then(source => { - outgoingCallLoop.current = source; + if (!useVoiceStore.getState().outgoingCall) { + source?.stop(); + } else { + outgoingCallLoop.current = source; + } + outgoingCallLoading.current = false; }); - } else if (!state.outgoingCall && outgoingCallLoop.current) { - outgoingCallLoop.current.stop(); - outgoingCallLoop.current = null; + } else if (!state.outgoingCall) { + if (outgoingCallLoop.current) { + outgoingCallLoop.current.stop(); + outgoingCallLoop.current = null; + } + outgoingCallLoading.current = false; } }); diff --git a/packages/web/src/hooks/useWebSocket.ts b/packages/web/src/hooks/useWebSocket.ts index 31e55b5e..a8910445 100644 --- a/packages/web/src/hooks/useWebSocket.ts +++ b/packages/web/src/hooks/useWebSocket.ts @@ -332,11 +332,10 @@ function handleEvent(origin: string, event: ServerEvent): void { if (call.federatedCallId) { setFederatedCallId(call.federatedCallId); } - if (connectFn && callDmId) { - connectFn(callDmId, true).catch((err) => { - console.error('[WS] DM call reconnect failed:', err); - }); - } + // PASSIVE: do NOT auto-connect to LiveKit on ready. + // The user must click "Join" or re-accept. Auto-connecting causes + // identity conflicts when the same user has multiple sessions — + // both sessions fight for the same LiveKit identity slot. break; } else if (call.state === 'ringing' && call.callerId !== myId) { const dmCh = event.dmChannels?.find((d: any) => d.id === call.dmChannelId);