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 */}

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;