import { useStore } from '../store'; import { Phone, PhoneOff, Mic, MicOff, Video, VideoOff } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useState, useEffect, useRef } from 'react'; import { socket } from '../socket'; export function CallOverlay() { const { activeCall, endCall, currentUser, incrementMissedCalls } = useStore(); const [micMuted, setMicMuted] = useState(false); const [videoMuted, setVideoMuted] = useState(false); const [stream, setStream] = useState(null); const [callAccepted, setCallAccepted] = useState(false); const [videoActive, setVideoActive] = useState(false); const [incomingVideoRequest, setIncomingVideoRequest] = useState(false); const [isRequestingVideo, setIsRequestingVideo] = useState(false); const localVideoRef = useRef(null); const remoteVideoRef = useRef(null); const connectionRef = useRef(null); const leaveCall = () => { if (connectionRef.current) connectionRef.current.close(); // Stop local media defensively via both state and active ref source if (stream) { stream.getTracks().forEach(track => track.stop()); } if (localVideoRef.current && localVideoRef.current.srcObject) { localVideoRef.current.srcObject.getTracks().forEach(track => track.stop()); localVideoRef.current.srcObject = null; } // Notify peer if (activeCall) { const peerId = activeCall.callerId === currentUser?.id ? activeCall.receiverId : activeCall.callerId; socket.emit('end-call', { to: peerId }); } setStream(null); setCallAccepted(false); endCall(); }; // We need to listen to incoming calls globally even if not in an active call state useEffect(() => { socket.on('incoming-call', (data) => { // Setup incoming state in store manually by injecting it useStore.setState({ activeCall: { callerId: data.from, callerName: data.callerName, callerSelfie: data.callerSelfie, receiverId: currentUser?.id, receiverName: currentUser?.name, receiverSelfie: currentUser?.selfiePath, status: 'ringing', signalData: data.signal // Stashed for answering } }); if ("Notification" in window && Notification.permission === "granted") { new Notification("Incoming Call \u260E\uFE0F", { body: `${data.callerName} is calling you on Meri Mandi!`, icon: data.callerSelfie || '/upiqr.jpeg' }); } }); socket.on('call-ended', () => { leaveCall(); }); socket.on('missed-call', () => { incrementMissedCalls(); leaveCall(); }); socket.on('video-requested', () => { setIncomingVideoRequest(true); }); socket.on('video-accepted', () => { setIsRequestingVideo(false); enableVideoTrack(); }); socket.on('video-rejected', () => { setIsRequestingVideo(false); alert("Video call request was declined."); }); return () => { socket.off('incoming-call'); socket.off('call-ended'); socket.off('missed-call'); socket.off('video-requested'); socket.off('video-accepted'); socket.off('video-rejected'); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentUser]); // Timeout logic (60 seconds) useEffect(() => { let timeoutId; if (activeCall && activeCall.status === 'ringing') { timeoutId = setTimeout(() => { const peerId = activeCall.callerId === currentUser?.id ? activeCall.receiverId : activeCall.callerId; if (activeCall.receiverId === currentUser?.id) { // I didn't answer - increment my missed calls incrementMissedCalls(); } // Notify other party socket.emit('missed-call', { to: peerId }); leaveCall(); }, 60000); } return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeCall]); // When activeCall becomes truthy and we are the CALLER (initiating), setup streams. useEffect(() => { if (activeCall && !stream && !callAccepted && activeCall.callerId === currentUser?.id) { navigator.mediaDevices.getUserMedia({ video: false, audio: true }).then((currentStream) => { setStream(currentStream); if (localVideoRef.current) localVideoRef.current.srcObject = currentStream; // Setup Peer Connection const peer = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); currentStream.getTracks().forEach(track => peer.addTrack(track, currentStream)); peer.ontrack = (event) => { if (remoteVideoRef.current) { remoteVideoRef.current.srcObject = event.streams[0]; } }; peer.onicecandidate = (event) => { if (event.candidate) { // ICE handling omitted for simplicity in this exact demo, or bundled in offer } }; peer.createOffer().then(offer => { peer.setLocalDescription(offer); socket.emit('call-user', { userToCall: activeCall.receiverId, signalData: offer, from: currentUser.id, callerName: currentUser.name, callerSelfie: currentUser.selfiePath }); }); socket.on('call-accepted', (signal) => { setCallAccepted(true); peer.setRemoteDescription(new RTCSessionDescription(signal)); }); connectionRef.current = peer; }).catch(err => { console.error("Failed to get media", err); alert("Camera and Mic permissions are required for calling"); endCall(); }); } // Cleanup when overlay completely unmounts return () => { if (!activeCall) { socket.off('call-accepted'); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeCall, stream, currentUser]); const answerCall = () => { if (!activeCall || !activeCall.signalData) return; setCallAccepted(true); useStore.setState({ activeCall: { ...activeCall, status: 'connected' } }); navigator.mediaDevices.getUserMedia({ video: false, audio: true }).then((currentStream) => { setStream(currentStream); if (localVideoRef.current) localVideoRef.current.srcObject = currentStream; const peer = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); currentStream.getTracks().forEach(track => peer.addTrack(track, currentStream)); peer.ontrack = (event) => { if (remoteVideoRef.current) { remoteVideoRef.current.srcObject = event.streams[0]; } }; peer.setRemoteDescription(new RTCSessionDescription(activeCall.signalData)).then(() => { peer.createAnswer().then(answer => { peer.setLocalDescription(answer); socket.emit('answer-call', { signal: answer, to: activeCall.callerId }); }); }); connectionRef.current = peer; }); }; const toggleMic = () => { if (stream) { stream.getAudioTracks()[0].enabled = micMuted; setMicMuted(!micMuted); } }; const requestVideo = () => { const peerId = activeCall.callerId === currentUser?.id ? activeCall.receiverId : activeCall.callerId; setIsRequestingVideo(true); socket.emit('request-video', { to: peerId, from: currentUser.id }); }; const acceptVideo = () => { const peerId = activeCall.callerId === currentUser?.id ? activeCall.receiverId : activeCall.callerId; setIncomingVideoRequest(false); socket.emit('accept-video', { to: peerId }); enableVideoTrack(); }; const rejectVideo = () => { const peerId = activeCall.callerId === currentUser?.id ? activeCall.receiverId : activeCall.callerId; setIncomingVideoRequest(false); socket.emit('reject-video', { to: peerId }); }; const enableVideoTrack = async () => { try { const videoStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); const videoTrack = videoStream.getVideoTracks()[0]; if (stream) { stream.addTrack(videoTrack); if (localVideoRef.current) localVideoRef.current.srcObject = stream; } if (connectionRef.current) { const senders = connectionRef.current.getSenders(); const videoSender = senders.find(s => s.track && s.track.kind === 'video'); if (videoSender) { videoSender.replaceTrack(videoTrack); } else { connectionRef.current.addTrack(videoTrack, stream); } } setVideoActive(true); setVideoMuted(false); } catch (err) { console.error("Failed to start video", err); } }; const toggleVideo = () => { if (!videoActive) { requestVideo(); return; } if (stream && stream.getVideoTracks().length > 0) { const track = stream.getVideoTracks()[0]; track.enabled = videoMuted; setVideoMuted(!videoMuted); } }; if (!activeCall) return null; const isRinging = activeCall.status === 'ringing'; const isIncoming = activeCall.receiverId === currentUser?.id && isRinging; const isOutgoing = activeCall.callerId === currentUser?.id; const peerSelfie = isOutgoing ? activeCall.receiverSelfie : activeCall.callerSelfie; return ( {/* Remote Video / Avatar Container */}
{/* Live Native Video */}
{incomingVideoRequest && (

Requesting Video Call...

)} {isRequestingVideo && ( Waiting for video call acceptance... )}

{isOutgoing ? activeCall.receiverName : activeCall.callerName}

{isRinging ? (isIncoming ? 'Incoming Call...' : 'Ringing...') : 'Connected Securely'}

{/* Action Controls */}
{(!isRinging || callAccepted) && ( <> )} {isIncoming && isRinging && (
)} {isOutgoing && isRinging && !callAccepted && ( )}
); }