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
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]) => (
))}
{/* 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
)}
);
}
// --- Helper Components ---
function VideoPlayer({ stream }) {
const videoRef = useRef(null);
useEffect(() => {
if (videoRef.current && stream) {
videoRef.current.srcObject = stream;
}
}, [stream]);
return (
);
}
function ControlButton({ onClick, active, icon, activeClass, inactiveClass, badge, title }) {
return (
);
}