import React, { useState, useEffect, useRef, useMemo } from 'react'; import { initializeApp } from 'firebase/app'; import { getFirestore, collection, onSnapshot, addDoc, doc, updateDoc, deleteDoc, serverTimestamp, query, orderBy, where, getDocs } from 'firebase/firestore'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth'; import { Mic, MicOff, Video, VideoOff, PhoneOff, Monitor, MessageSquare, Users, Copy, Check, Share2, Settings } from 'lucide-react'; // --- Firebase Configuration & Initialization --- const firebaseConfig = JSON.parse(__firebase_config); const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // --- WebRTC Configuration --- const rtcConfig = { iceServers: [ { urls: ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'] } ] }; export default function OpenMeet() { const [user, setUser] = useState(null); const [joined, setJoined] = useState(false); const [roomId, setRoomId] = useState(''); const [userName, setUserName] = useState(''); // Initialize Auth useEffect(() => { const initAuth = async () => { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, setUser); return () => unsubscribe(); }, []); if (!user) { return (

Initializing Secure Connection...

); } return joined ? ( setJoined(false)} /> ) : ( setJoined(true)} /> ); } // --- Lobby Component --- function Lobby({ userName, setUserName, roomId, setRoomId, onJoin }) { const generateRoomId = () => Math.random().toString(36).substring(2, 8).toUpperCase(); return (

OpenMeet

Secure, P2P, Unlimited Video Conferencing

setUserName(e.target.value)} />
Join or Create
setRoomId(e.target.value.toUpperCase())} />

No signup required • Unlimited participants • P2P Encrypted

); } // --- Main Meeting Room Component --- function MeetingRoom({ user, roomId, userName, onLeave }) { const [localStream, setLocalStream] = useState(null); const [peers, setPeers] = useState({}); // { [userId]: { stream, userName } } const [audioEnabled, setAudioEnabled] = useState(true); const [videoEnabled, setVideoEnabled] = useState(true); const [screenSharing, setScreenSharing] = useState(false); const [messages, setMessages] = useState([]); const [showChat, setShowChat] = useState(false); const [newMessage, setNewMessage] = useState(""); const [copied, setCopied] = useState(false); // Refs for WebRTC management const pcsRef = useRef({}); // { [peerId]: RTCPeerConnection } const localStreamRef = useRef(null); const peersRef = useRef({}); // To access current peers in closures // Determine collection name based on room (Strict Path Rule) // We use a safe alphanumeric version of room ID const safeRoomId = roomId.replace(/[^a-zA-Z0-9]/g, ''); const collectionName = `meet_${safeRoomId}`; // --- 1. Setup Local Stream --- useEffect(() => { const startStream = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); setLocalStream(stream); localStreamRef.current = stream; // Add presence to Firestore await addDoc(collection(db, 'artifacts', appId, 'public', 'data', collectionName), { type: 'join', userId: user.uid, userName: userName, timestamp: serverTimestamp() }); } catch (err) { console.error("Error accessing media devices:", err); alert("Could not access camera/microphone. Please allow permissions."); } }; startStream(); // Cleanup on unmount return () => { if (localStreamRef.current) { localStreamRef.current.getTracks().forEach(track => track.stop()); } // Ideally we would delete the join doc here, but difficult reliably on close }; }, []); // Run once on mount // --- 2. Signaling Logic (The Core P2P Mesh) --- useEffect(() => { if (!user) return; // Listen to the room's signal collection const q = query( collection(db, 'artifacts', appId, 'public', 'data', collectionName), orderBy('timestamp', 'asc') // Process in order ); const unsubscribe = onSnapshot(q, async (snapshot) => { snapshot.docChanges().forEach(async (change) => { if (change.type === 'added') { const data = change.doc.data(); // Ignore our own messages if (data.userId === user.uid) return; // A. New User Joined -> Initiate Connection (if I'm the "older" peer or just based on ID sort to avoid duplicate pairs) // We use a simple rule: The existing users connect to the new user. // However, in a pure event log, everyone sees everyone "added". // Strategy: Connect if we don't have a PC yet. // To avoid glare (both offering), we use ID comparison string sort. // If myID > theirID, I offer. if (data.type === 'join') { handleUserJoined(data.userId, data.userName); } if (data.to === user.uid) { if (data.type === 'offer') { await handleOffer(data); } else if (data.type === 'answer') { await handleAnswer(data); } else if (data.type === 'candidate') { await handleCandidate(data); } } if (data.type === 'chat') { setMessages(prev => [...prev, { sender: data.userName, text: data.text, time: data.timestamp }]); } } }); }, (error) => { console.error("Signaling error:", error); }); return () => unsubscribe(); }, [user]); // --- WebRTC Handlers --- const createPeerConnection = (targetId, targetName) => { if (pcsRef.current[targetId]) return pcsRef.current[targetId]; const pc = new RTCPeerConnection(rtcConfig); // Add local tracks if (localStreamRef.current) { localStreamRef.current.getTracks().forEach(track => { pc.addTrack(track, localStreamRef.current); }); } // Handle ICE candidates pc.onicecandidate = (event) => { if (event.candidate) { addDoc(collection(db, 'artifacts', appId, 'public', 'data', collectionName), { type: 'candidate', userId: user.uid, to: targetId, candidate: JSON.stringify(event.candidate), timestamp: serverTimestamp() }); } }; // Handle remote stream pc.ontrack = (event) => { console.log(`Received track from ${targetName}`); setPeers(prev => ({ ...prev, [targetId]: { stream: event.streams[0], userName: targetName } })); peersRef.current = { ...peersRef.current, [targetId]: { userName: targetName }}; }; // Handle connection state pc.onconnectionstatechange = () => { if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') { setPeers(prev => { const newPeers = { ...prev }; delete newPeers[targetId]; return newPeers; }); delete pcsRef.current[targetId]; } }; pcsRef.current[targetId] = pc; return pc; }; const handleUserJoined = async (targetId, targetName) => { // If I have a larger ID, I initiate the offer. This prevents collision. // Simpler approach for this demo: If I'm already here and see a join, I offer. // But since 'join' events replay, we need to be careful. // Let's stick to: If my ID > their ID, I offer. if (user.uid > targetId) { const pc = createPeerConnection(targetId, targetName); const offer = await pc.createOffer(); await pc.setLocalDescription(offer); await addDoc(collection(db, 'artifacts', appId, 'public', 'data', collectionName), { type: 'offer', userId: user.uid, userName: userName, to: targetId, sdp: JSON.stringify(offer), timestamp: serverTimestamp() }); } }; const handleOffer = async (data) => { const pc = createPeerConnection(data.userId, data.userName); await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data.sdp))); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); await addDoc(collection(db, 'artifacts', appId, 'public', 'data', collectionName), { type: 'answer', userId: user.uid, to: data.userId, sdp: JSON.stringify(answer), timestamp: serverTimestamp() }); }; const handleAnswer = async (data) => { const pc = pcsRef.current[data.userId]; if (pc) { await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data.sdp))); } }; const handleCandidate = async (data) => { const pc = pcsRef.current[data.userId]; if (pc) { await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(data.candidate))); } }; // --- Features --- const toggleAudio = () => { if (localStream) { localStream.getAudioTracks().forEach(track => track.enabled = !audioEnabled); setAudioEnabled(!audioEnabled); } }; const toggleVideo = () => { if (localStream) { localStream.getVideoTracks().forEach(track => track.enabled = !videoEnabled); setVideoEnabled(!videoEnabled); } }; const toggleScreenShare = async () => { if (!screenSharing) { try { const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true }); const videoTrack = screenStream.getVideoTracks()[0]; // Replace track in all PeerConnections Object.values(pcsRef.current).forEach(pc => { const sender = pc.getSenders().find(s => s.track.kind === 'video'); if (sender) sender.replaceTrack(videoTrack); }); // Update local view setLocalStream(screenStream); // Temporarily show screen locally setScreenSharing(true); setVideoEnabled(true); // Handle stop sharing via browser UI videoTrack.onended = () => { stopScreenShare(); }; } catch (err) { console.error("Error sharing screen", err); } } else { stopScreenShare(); } }; const stopScreenShare = async () => { // Revert to camera try { const camStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); const videoTrack = camStream.getVideoTracks()[0]; Object.values(pcsRef.current).forEach(pc => { const sender = pc.getSenders().find(s => s.track.kind === 'video'); if (sender) sender.replaceTrack(videoTrack); }); setLocalStream(camStream); localStreamRef.current = camStream; setScreenSharing(false); setVideoEnabled(true); } catch (err) { console.error("Error reverting to camera", err); } }; const sendMessage = async (e) => { e.preventDefault(); if (!newMessage.trim()) return; await addDoc(collection(db, 'artifacts', appId, 'public', 'data', collectionName), { type: 'chat', userId: user.uid, userName: userName, text: newMessage, timestamp: serverTimestamp() }); setNewMessage(""); }; const copyRoomId = () => { const text = roomId; // Use fallback for clipboard in iframe const textArea = document.createElement("textarea"); textArea.value = text; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Fallback: Oops, unable to copy', err); } document.body.removeChild(textArea); }; return (
{/* Main Video Area */}
{/* Header */}

OpenMeet

{Object.keys(peers).length + 1} online
{/* Video Grid */}
{/* My Video */}
{/* Peer Videos */} {Object.entries(peers).map(([id, peer]) => (
{peer.userName}
))} {/* Empty State / Waiting */} {Object.keys(peers).length === 0 && (

Waiting for others to join...

Share Room ID: {roomId}

)}
{/* Controls Bar */}
: } activeClass="bg-gray-700 hover:bg-gray-600" inactiveClass="bg-red-500 hover:bg-red-600 text-white border-transparent" /> : } activeClass="bg-gray-700 hover:bg-gray-600" inactiveClass="bg-red-500 hover:bg-red-600 text-white border-transparent" /> } activeClass="bg-gray-700 hover:bg-gray-600" inactiveClass="bg-green-500 hover:bg-green-600 text-white border-transparent" title="Share Screen" /> setShowChat(!showChat)} active={!showChat} icon={} activeClass="bg-gray-700 hover:bg-gray-600" inactiveClass="bg-blue-500 hover:bg-blue-600 text-white border-transparent" badge={!showChat && messages.length > 0} />
{/* Chat Sidebar */}

Meeting Chat

{messages.map((msg, idx) => (
{msg.sender} {msg.time ? new Date(msg.time.seconds * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : ''}
{msg.text}
))} {messages.length === 0 && (
No messages yet
)}
setNewMessage(e.target.value)} placeholder="Type a message..." className="w-full pl-4 pr-10 py-3 bg-gray-700 border border-gray-600 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none text-white text-sm" />
); } // --- Helper Components --- function VideoPlayer({ stream }) { const videoRef = useRef(null); useEffect(() => { if (videoRef.current && stream) { videoRef.current.srcObject = stream; } }, [stream]); return (