import React, { useEffect, useRef, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import io from 'socket.io-client'; import Peer from 'simple-peer'; import { roomAPI } from '../utils/api'; import { useAuth } from '../context/AuthContext'; // --- Icons --- const MicIcon = ({ className }) => ( ); const MicOffIcon = ({ className }) => ( ); const VideoIcon = ({ className }) => ( ); const VideoOffIcon = ({ className }) => ( ); const MonitorIcon = ({ className }) => ( ); const PhoneOffIcon = ({ className }) => ( ); const CopyIcon = ({ className }) => ( ); const PinIcon = ({ className, filled }) => ( ); // --- Video Player Component --- const VideoPlayer = ({ stream, isLocal = false, onPin, isPinned, label }) => { const ref = useRef(); useEffect(() => { if (ref.current && stream) { ref.current.srcObject = stream; } }, [stream]); return (
); }; const VideoRoom = () => { const { roomId } = useParams(); const { user } = useAuth(); const navigate = useNavigate(); // State const [peers, setPeers] = useState([]); // Array of { peerID, peer } const [remoteStreams, setRemoteStreams] = useState([]); // Array of { id, stream, peerID } const [userStream, setUserStream] = useState(null); const [screenStream, setScreenStream] = useState(null); const [pinnedStreamId, setPinnedStreamId] = useState(null); // ID of pinned stream // Controls const [isMuted, setIsMuted] = useState(false); const [isVideoOff, setIsVideoOff] = useState(false); const [isScreenSharing, setIsScreenSharing] = useState(false); const [roomInfo, setRoomInfo] = useState(null); const [showCopied, setShowCopied] = useState(false); // Refs const socketRef = useRef(); const peersRef = useRef([]); // Keep track of peers for cleanup const userStreamRef = useRef(); const screenStreamRef = useRef(); // Corrected declaration const isMounted = useRef(true); // Track mount status // --- Initialization & Cleanup --- useEffect(() => { isMounted.current = true; // Prevent accidental refresh const handleBeforeUnload = (e) => { e.preventDefault(); e.returnValue = ''; }; window.addEventListener('beforeunload', handleBeforeUnload); const init = async () => { try { await fetchRoomInfo(); if (isMounted.current) { await initializeMedia(); } } catch (err) { console.error(err); } }; init(); return () => { isMounted.current = false; window.removeEventListener('beforeunload', handleBeforeUnload); cleanupConnection(); }; }, [roomId]); const cleanupConnection = () => { console.log("Cleaning up connections..."); // 1. Stop Local Camera if (userStreamRef.current) { userStreamRef.current.getTracks().forEach(track => { track.stop(); track.enabled = false; }); userStreamRef.current = null; } // 2. Stop Screen Share if (screenStreamRef.current) { screenStreamRef.current.getTracks().forEach(track => track.stop()); screenStreamRef.current = null; } // 3. Destroy Peers peersRef.current.forEach(({ peer }) => { if (peer && !peer.destroyed) { peer.destroy(); } }); peersRef.current = []; // Clear state only if mounted to avoid react warnings if (isMounted.current) { setPeers([]); setRemoteStreams([]); setUserStream(null); setScreenStream(null); } // 4. Disconnect Socket if (socketRef.current) { socketRef.current.disconnect(); socketRef.current = null; } }; const fetchRoomInfo = async () => { try { const response = await roomAPI.getRoom(roomId); if (isMounted.current) { setRoomInfo(response.data.room); await roomAPI.joinRoom(roomId); } } catch (error) { console.error('Error fetching room:', error); if (isMounted.current) navigate('/dashboard'); } }; // --- WebRTC Logic --- const initializeMedia = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); // CRITICAL: Check if still mounted after async call if (!isMounted.current) { // If unmounted during getUserMedia, immediately stop these tracks stream.getTracks().forEach(t => t.stop()); return; } // If there was an old stream pending in ref (race condition), stop it if (userStreamRef.current) { userStreamRef.current.getTracks().forEach(t => t.stop()); } userStreamRef.current = stream; setUserStream(stream); socketRef.current = io(process.env.REACT_APP_SOCKET_URL); socketRef.current.emit('join-room', roomId, user.id); // --- Socket Events --- socketRef.current.on('user-connected', (userId) => { if (!isMounted.current) return; const peer = createPeer(userId, socketRef.current.id, stream); peersRef.current.push({ peerID: userId, peer }); setPeers(prev => [...prev, { peerID: userId, peer }]); }); socketRef.current.on('user-disconnected', (userId) => { if (!isMounted.current) return; const peerObj = peersRef.current.find(p => p.peerID === userId); if (peerObj && peerObj.peer) peerObj.peer.destroy(); peersRef.current = peersRef.current.filter(p => p.peerID !== userId); setPeers(prev => prev.filter(p => p.peerID !== userId)); setRemoteStreams(prev => prev.filter(s => s.peerID !== userId)); }); socketRef.current.on('offer', (offer, id) => { if (!isMounted.current) return; const peer = addPeer(offer, id, stream); peersRef.current.push({ peerID: id, peer }); setPeers(prev => [...prev, { peerID: id, peer }]); }); socketRef.current.on('answer', (answer, id) => { const p = peersRef.current.find(p => p.peerID === id); if (p) p.peer.signal(answer); }); socketRef.current.on('ice-candidate', (candidate, id) => { const p = peersRef.current.find(p => p.peerID === id); if (p) p.peer.signal(candidate); }); // Notification only - stream handling is done via peer.on('stream') socketRef.current.on('screen-share-started', (id) => { console.log(`User ${id} started screen sharing`); }); socketRef.current.on('screen-share-stopped', (id) => { console.log(`User ${id} stopped screen sharing`); setRemoteStreams(prev => prev.filter(s => { // Keep streams; simple-peer usually removes track automatically or we can add logic here if needed return true; })); }); } catch (error) { console.error('Media Error:', error); if (isMounted.current) { alert('Could not access camera/microphone'); navigate('/dashboard'); } } }; const createPeer = (userToSignal, callerID, stream) => { const peer = new Peer({ initiator: true, trickle: false, stream, }); peer.on('signal', signal => { socketRef.current.emit('offer', signal, roomId); }); peer.on('stream', remoteStream => { handleRemoteStream(remoteStream, userToSignal); }); return peer; }; const addPeer = (incomingSignal, callerID, stream) => { const peer = new Peer({ initiator: false, trickle: false, stream, }); peer.on('signal', signal => { socketRef.current.emit('answer', signal, roomId); }); peer.on('stream', remoteStream => { handleRemoteStream(remoteStream, callerID); }); peer.signal(incomingSignal); return peer; }; const handleRemoteStream = (stream, peerID) => { setRemoteStreams(prev => { if (prev.some(s => s.id === stream.id)) return prev; return [...prev, { id: stream.id, stream, peerID }]; }); }; // --- Controls --- const toggleMute = () => { if (userStreamRef.current) { const track = userStreamRef.current.getAudioTracks()[0]; if (track) { track.enabled = !track.enabled; setIsMuted(!track.enabled); } } }; const toggleVideo = () => { if (userStreamRef.current) { const track = userStreamRef.current.getVideoTracks()[0]; if (track) { track.enabled = !track.enabled; setIsVideoOff(!track.enabled); } } }; const startScreenShare = async () => { try { const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); screenStreamRef.current = stream; setScreenStream(stream); setIsScreenSharing(true); setPinnedStreamId('local-screen'); // Auto-pin my screen // Add stream to all peers peersRef.current.forEach(({ peer }) => { peer.addStream(stream); }); // Handle native stop button stream.getVideoTracks()[0].onended = () => stopScreenShare(); socketRef.current.emit('screen-share-started', roomId); } catch (error) { console.error("Screen share failed", error); } }; const stopScreenShare = () => { if (screenStreamRef.current) { // Remove stream from peers peersRef.current.forEach(({ peer }) => { peer.removeStream(screenStreamRef.current); }); // Stop tracks screenStreamRef.current.getTracks().forEach(t => t.stop()); screenStreamRef.current = null; } setScreenStream(null); setIsScreenSharing(false); // Unpin if necessary setPinnedStreamId(prev => (prev === 'local-screen' ? null : prev)); socketRef.current.emit('screen-share-stopped', roomId); }; const leaveRoom = () => { cleanupConnection(); navigate('/dashboard'); }; const copyLink = () => { const link = `${window.location.origin}/room/${roomId}`; navigator.clipboard.writeText(link); setShowCopied(true); setTimeout(() => setShowCopied(false), 2000); }; // --- Render Helpers --- const getAllStreams = () => { const streams = []; // 1. Local Camera if (userStream) { streams.push({ id: 'local-cam', stream: userStream, isLocal: true, label: 'You' }); } // 2. Local Screen if (screenStream) { streams.push({ id: 'local-screen', stream: screenStream, isLocal: true, label: 'Your Screen' }); } // 3. Remote Streams remoteStreams.forEach(rs => { streams.push({ id: rs.id, stream: rs.stream, isLocal: false, label: `Participant ${rs.peerID.substr(0,4)}` }); }); return streams; }; const allStreams = getAllStreams(); const pinnedStream = allStreams.find(s => s.id === pinnedStreamId); return (
{/* Header */}
Logo e.target.style.display='none'} />

{roomInfo?.name || 'Meeting Room'}

{roomId}
{/* Main Content Area */}
{pinnedStream ? ( // Pinned Layout
{/* Main Stage */}
setPinnedStreamId(null)} // Unpin />
{/* Side Filmstrip */}
{allStreams.filter(s => s.id !== pinnedStreamId).map(s => (
setPinnedStreamId(s.id)} />
))}
) : ( // Grid Layout
{allStreams.map(s => (
setPinnedStreamId(s.id)} />
))} {allStreams.length === 0 && (
Waiting for others...
)}
)}
{/* Footer Controls */}
); }; export default VideoRoom;