diff --git a/tauri/src/components/ui/call-center.tsx b/tauri/src/components/ui/call-center.tsx index 69339af6..36cd35e0 100644 --- a/tauri/src/components/ui/call-center.tsx +++ b/tauri/src/components/ui/call-center.tsx @@ -1,5 +1,5 @@ import { formatDistanceToNow } from "date-fns"; -import { LuMicOff, LuVideo, LuVideoOff, LuScreenShare, LuScreenShareOff } from "react-icons/lu"; +import { LuMicOff, LuVideo, LuVideoOff, LuScreenShare, LuScreenShareOff, LuWifiOff } from "react-icons/lu"; import { PiScribbleLoopBold } from "react-icons/pi"; import useStore, { CallState, ParticipantRole } from "@/store/store"; import { useKrispNoiseFilter } from "@livekit/components-react/krisp"; @@ -26,6 +26,8 @@ import { LocalTrack, ParticipantEvent, AudioPresets, + EngineEvent, + RemoteParticipant, } from "livekit-client"; import { useCallback, useEffect, useRef, useState } from "react"; import { Select, SelectContent, SelectItem, SelectTrigger } from "./select"; @@ -63,7 +65,7 @@ export function CallCenter() {
{/* Reconnecting Banner */} {callTokens.isReconnecting && ( -
+
Reconnecting...
@@ -97,7 +99,9 @@ export function ConnectedActions() { const callParticipant = teammates?.find((user) => user.id === callTokens?.participant); const [controllerCursorState, setControllerCursorState] = useState(true); const [accessibilityPermission, setAccessibilityPermission] = useState(true); + const [isRemoteDisconnected, setIsRemoteDisconnected] = useState(false); const remoteParticipants = useRemoteParticipants(); + const room = useRoomContext(); // Find the remote audio participant and check if they have microphone enabled const remoteAudioParticipant = remoteParticipants.find( @@ -108,6 +112,36 @@ export function ConnectedActions() { useScreenShareListener(); const handleEndCall = useEndCall(); + // Listen for participant disconnection events + useEffect(() => { + if (!callParticipant) return; + + const handleParticipantDisconnected = (participant: RemoteParticipant) => { + if (participant.identity.includes("audio") && participant.identity.includes(callParticipant.id)) { + console.log("Remote participant disconnected:", participant.identity); + setIsRemoteDisconnected(true); + } + }; + + const handleParticipantConnected = (participant: RemoteParticipant) => { + if (participant.identity.includes("audio") && participant.identity.includes(callParticipant.id)) { + console.log("Remote participant connected:", participant.identity); + setIsRemoteDisconnected(false); + } + }; + + room.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected); + room.on(RoomEvent.ParticipantConnected, handleParticipantConnected); + + // Initialize state based on current participants + setIsRemoteDisconnected(false); + + return () => { + room.off(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected); + room.off(RoomEvent.ParticipantConnected, handleParticipantConnected); + }; + }, [room, callParticipant?.id]); + const fetchAccessibilityPermission = async () => { const permission = await tauriUtils.getControlPermission(); setAccessibilityPermission(permission); @@ -126,15 +160,6 @@ export function ConnectedActions() { fetchAccessibilityPermission(); }, [callTokens?.role]); - // Stop call when teammate disconnects - useEffect(() => { - if (!callTokens || !callParticipant) return; - - if (!callParticipant.is_active) { - handleEndCall(); - } - }, [callParticipant, teammates, callTokens, handleEndCall]); - return ( <> {/* */} @@ -148,12 +173,19 @@ export function ConnectedActions() {
{callParticipant && ( - + <> + + {isRemoteDisconnected && ( +
+ +
+ )} + )}
@@ -703,7 +735,7 @@ function CameraIcon() { } function MediaDevicesSettings() { - const { callTokens, setCallTokens, updateCallTokens, livekitUrl } = useStore(); + const { callTokens, setCallTokens, updateCallTokens, livekitUrl, socketConnected } = useStore(); const { state: roomState } = useRoomContext(); const { localParticipant } = useLocalParticipant(); const { isNoiseFilterPending, setNoiseFilterEnabled, isNoiseFilterEnabled } = useKrispNoiseFilter({ @@ -721,11 +753,29 @@ function MediaDevicesSettings() { const [roomConnected, setRoomConnected] = useState(false); useEffect(() => { - room.on(RoomEvent.Connected, () => { + const handleConnected = () => { setRoomConnected(true); - }); + }; + + room.on(RoomEvent.Connected, handleConnected); + + return () => { + room.off(RoomEvent.Connected, handleConnected); + }; }, [room]); + // Early disconnection detection via SocketService + useEffect(() => { + if (!callTokens) return; + + if (!socketConnected && !callTokens.isReconnecting) { + updateCallTokens({ isReconnecting: true }); + } else if (socketConnected && callTokens.isReconnecting && roomState === ConnectionState.Connected) { + // Socket recovered and LiveKit is still connected — clear the banner + updateCallTokens({ isReconnecting: false }); + } + }, [socketConnected, callTokens, updateCallTokens, roomState]); + // Listen to connection state changes and handle reconnection useEffect(() => { const handleConnectionStateChange = async (state: ConnectionState) => { @@ -745,8 +795,8 @@ function MediaDevicesSettings() { console.error("Reconnection failed:", error); updateCallTokens({ isReconnecting: false }); } - } else if (state === ConnectionState.Connected && callTokens?.isReconnecting) { - // Successfully reconnected + } else if (state === ConnectionState.Connected && callTokens?.isReconnecting && socketConnected) { + // Successfully reconnected (both LiveKit and socket are connected) console.log("Successfully reconnected!"); updateCallTokens({ isReconnecting: false }); } @@ -757,7 +807,7 @@ function MediaDevicesSettings() { return () => { room.off(RoomEvent.ConnectionStateChanged, handleConnectionStateChange); }; - }, [room, callTokens, updateCallTokens, livekitUrl]); + }, [room, callTokens, updateCallTokens, livekitUrl, socketConnected]); useEffect(() => { if (!callTokens) return; @@ -787,7 +837,7 @@ function MediaDevicesSettings() { videoCodec: "h264", simulcast: true, videoEncoding: { - maxBitrate: 1_300_000, + maxBitrate: 1_700_000, }, videoSimulcastLayers: [VideoPresets.h360, VideoPresets.h216], },