![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/private_html/src/components/ |
import React, { useRef, useEffect, useState } from 'react';
import { useWebSocket } from '../context/StableWebSocketContext';
import { motion, AnimatePresence } from 'framer-motion';
import Peer from 'simple-peer';
const SimpleVideoCall: React.FC = () => {
const { videoCallActive, currentVideoCall, endVideoCall, ws } = useWebSocket();
const localVideoRef = useRef<HTMLVideoElement>(null);
const remoteVideoRef = useRef<HTMLVideoElement>(null);
const peerRef = useRef<Peer.Instance | null>(null);
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'failed'>('connecting');
const [isLocalMuted, setIsLocalMuted] = useState(false);
const [isLocalVideoOff, setIsLocalVideoOff] = useState(false);
const [callDuration, setCallDuration] = useState(0);
const [callStartTime] = useState<number>(Date.now());
// ✅ Update call timer every second
useEffect(() => {
if (!videoCallActive) return;
const timer = setInterval(() => {
setCallDuration(Math.floor((Date.now() - callStartTime) / 1000));
}, 1000);
return () => clearInterval(timer);
}, [videoCallActive, callStartTime]);
// ✅ Format call duration (mm:ss)
const formatCallDuration = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// ✅ Get media stream when call starts
useEffect(() => {
if (videoCallActive) {
console.log('📞 [SIMPLE] Starting media capture');
startMediaCapture();
} else {
console.log('📞 [SIMPLE] Stopping media capture');
stopMediaCapture();
}
}, [videoCallActive]);
// ✅ Setup WebRTC peer connection when we have a stream
useEffect(() => {
if (localStream && currentVideoCall && !peerRef.current) {
console.log('📞 [SIMPLE] Setting up WebRTC peer connection');
setupPeerConnection();
}
}, [localStream, currentVideoCall]);
// ✅ WebSocket message handling for WebRTC signaling
useEffect(() => {
if (!ws) return;
const handleMessage = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data);
if (message.senderId === currentVideoCall?.recipientId) {
switch (message.type) {
case 'webrtc-offer':
case 'webrtc-answer':
if (peerRef.current && message.data.signal) {
console.log('📞 [SIMPLE] Received signaling data:', message.type);
peerRef.current.signal(message.data.signal);
}
break;
case 'webrtc-ice-candidate':
if (peerRef.current && message.data.candidate) {
console.log('📞 [SIMPLE] Received ICE candidate');
peerRef.current.signal(message.data.candidate);
}
break;
case 'webrtc-end-call':
console.log('📞 [SIMPLE] Remote user ended call');
handleEndCall();
break;
}
}
} catch (error) {
console.error('📞 [SIMPLE] Error handling WebSocket message:', error);
}
};
ws.addEventListener('message', handleMessage);
return () => ws.removeEventListener('message', handleMessage);
}, [ws, currentVideoCall?.recipientId]);
const startMediaCapture = async () => {
try {
console.log('📞 [SIMPLE] Requesting camera and microphone...');
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
console.log('📞 [SIMPLE] Got media stream:', stream);
setLocalStream(stream);
if (localVideoRef.current) {
localVideoRef.current.srcObject = stream;
}
} catch (error) {
console.error('📞 [SIMPLE] Failed to get media:', error);
// Create mock stream for development
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
const ctx = canvas.getContext('2d');
let frame = 0;
const animate = () => {
if (ctx) {
ctx.fillStyle = '#1f2937';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const x = canvas.width / 2 + Math.sin(frame * 0.05) * 100;
const y = canvas.height / 2 + Math.cos(frame * 0.03) * 50;
ctx.fillStyle = '#3b82f6';
ctx.beginPath();
ctx.arc(x, y, 30, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.fillText('🎭 DEV MODE - Mock Video', canvas.width / 2, canvas.height / 2 + 100);
frame++;
}
requestAnimationFrame(animate);
};
animate();
const mockStream = canvas.captureStream(30);
setLocalStream(mockStream);
if (localVideoRef.current) {
localVideoRef.current.srcObject = mockStream;
}
}
};
const stopMediaCapture = () => {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
setLocalStream(null);
}
if (remoteStream) {
setRemoteStream(null);
}
if (peerRef.current) {
peerRef.current.destroy();
peerRef.current = null;
}
};
const toggleMute = () => {
if (localStream) {
const audioTracks = localStream.getAudioTracks();
audioTracks.forEach(track => {
track.enabled = isLocalMuted;
});
setIsLocalMuted(!isLocalMuted);
}
};
const toggleVideo = () => {
if (localStream) {
const videoTracks = localStream.getVideoTracks();
videoTracks.forEach(track => {
track.enabled = isLocalVideoOff;
});
setIsLocalVideoOff(!isLocalVideoOff);
}
};
const handleEndCall = () => {
console.log('📞 [SIMPLE] Ending call');
// Send end call signal to remote user
if (ws?.readyState === WebSocket.OPEN && currentVideoCall) {
ws.send(JSON.stringify({
type: 'webrtc-end-call',
data: {},
recipientId: currentVideoCall.recipientId
}));
}
stopMediaCapture();
endVideoCall();
};
const setupPeerConnection = () => {
if (!localStream || !currentVideoCall) return;
const peer = new Peer({
initiator: currentVideoCall.isInitiator,
trickle: true,
stream: localStream,
});
peer.on('signal', (signal) => {
console.log('📞 [SIMPLE] Sending WebRTC signal');
const messageType = currentVideoCall.isInitiator ? 'webrtc-offer' : 'webrtc-answer';
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: messageType,
data: { signal },
senderId: 'current-user', // Will be set by server
recipientId: currentVideoCall.recipientId
}));
}
});
peer.on('stream', (stream) => {
console.log('📞 [SIMPLE] Received remote stream');
setRemoteStream(stream);
setConnectionStatus('connected');
if (remoteVideoRef.current) {
remoteVideoRef.current.srcObject = stream;
}
});
peer.on('connect', () => {
console.log('📞 [SIMPLE] Peer connected');
setConnectionStatus('connected');
});
peer.on('error', (error) => {
console.error('📞 [SIMPLE] Peer error:', error);
setConnectionStatus('failed');
});
peerRef.current = peer;
};
if (!videoCallActive || !currentVideoCall) return null;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-gray-900 flex flex-col z-[9999]"
>
{/* Header */}
<div className="bg-gray-800 text-white p-4 flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold flex items-center gap-2">
🎥 Video Call with {currentVideoCall.recipientName}
</h2>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${
connectionStatus === 'connected' ? 'bg-green-500 animate-pulse' :
connectionStatus === 'connecting' ? 'bg-yellow-500 animate-pulse' :
'bg-red-500'
}`}></div>
<p className="text-sm text-gray-300">
{connectionStatus === 'connected' ? 'Live' :
connectionStatus === 'connecting' ? 'Connecting' :
'Failed'}
</p>
</div>
<div className="flex items-center gap-2 bg-gray-700 px-3 py-1 rounded-lg">
<span className="text-sm font-mono text-white">
{formatCallDuration(callDuration)}
</span>
</div>
</div>
</div>
<button
onClick={handleEndCall}
className="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg transition-all duration-200 flex items-center gap-2 hover:scale-105 transform"
>
End Call
</button>
</div>
{/* Video Area */}
<div className="flex-1 relative bg-black">
{/* Remote Video */}
{remoteStream ? (
<video
ref={remoteVideoRef}
autoPlay
playsInline
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-white">
<div className="text-center">
{connectionStatus === 'connecting' && (
<>
<div className="w-16 h-16 border-4 border-t-transparent border-blue-500 rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-xl mb-2">🔄 Connecting to {currentVideoCall.recipientName}...</p>
<p className="text-sm text-gray-400">Setting up video connection</p>
</>
)}
{connectionStatus === 'failed' && (
<>
<div className="text-red-500 text-6xl mb-4">❌</div>
<p className="text-xl mb-2">Connection Failed</p>
<p className="text-sm text-gray-400">Unable to connect to {currentVideoCall.recipientName}</p>
</>
)}
{connectionStatus === 'connected' && !remoteStream && (
<>
<div className="text-green-500 text-6xl mb-4">📞</div>
<p className="text-xl mb-2">Connected!</p>
<p className="text-sm text-gray-400">Waiting for {currentVideoCall.recipientName}'s video...</p>
</>
)}
<div className="mt-4 text-xs text-gray-500">
Call duration: {formatCallDuration(callDuration)}
</div>
</div>
</div>
)}
{/* Local Video */}
{localStream && (
<div className="absolute top-4 right-4 w-48 h-36 bg-gray-800 rounded-lg overflow-hidden border-2 border-gray-600 shadow-2xl">
{isLocalVideoOff ? (
<div className="w-full h-full flex items-center justify-center bg-gray-700">
<span className="text-gray-400">📹</span>
</div>
) : (
<video
ref={localVideoRef}
autoPlay
playsInline
muted
className="w-full h-full object-cover"
/>
)}
<div className="absolute bottom-2 left-2 text-xs text-white bg-black bg-opacity-75 px-2 py-1 rounded">
📹 You
</div>
{isLocalMuted && (
<div className="absolute top-2 right-2 bg-red-600 rounded-full p-1 text-xs">
🔇
</div>
)}
</div>
)}
</div>
{/* Controls */}
<div className="bg-gray-800 p-6 flex items-center justify-center space-x-6">
<button
onClick={toggleMute}
className={`w-14 h-14 rounded-full flex items-center justify-center transition-all duration-200 transform hover:scale-110 ${
isLocalMuted
? 'bg-red-600 hover:bg-red-700 ring-2 ring-red-400'
: 'bg-gray-600 hover:bg-gray-700'
}`}
title={isLocalMuted ? 'Unmute' : 'Mute'}
>
{isLocalMuted ? '🔇' : '🎤'}
</button>
<button
onClick={toggleVideo}
className={`w-14 h-14 rounded-full flex items-center justify-center transition-all duration-200 transform hover:scale-110 ${
isLocalVideoOff
? 'bg-red-600 hover:bg-red-700 ring-2 ring-red-400'
: 'bg-gray-600 hover:bg-gray-700'
}`}
title={isLocalVideoOff ? 'Turn on camera' : 'Turn off camera'}
>
{isLocalVideoOff ? '📷' : '📹'}
</button>
<button
onClick={handleEndCall}
className="w-16 h-16 rounded-full bg-red-600 hover:bg-red-700 flex items-center justify-center transition-all duration-200 transform hover:scale-110 ring-2 ring-red-400 text-2xl"
title="End call"
>
📞
</button>
</div>
</motion.div>
</AnimatePresence>
);
};
export default SimpleVideoCall;