{status === 'calling' ? 'Conectando...' : 'Esperando conexión remota...'}
import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getFirestore, doc, setDoc, getDoc, onSnapshot, collection, addDoc, updateDoc } from 'firebase/firestore'; import { getAuth, signInAnonymously, onAuthStateChanged } from 'firebase/auth'; import { Video, VideoOff, Mic, MicOff, Phone, PhoneOff, Copy, UserPlus } from 'lucide-react'; // Configuración de Firebase desde el entorno 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 : 'video-call-default'; // Servidores STUN gratuitos de Google para facilitar la conexión P2P const servers = { iceServers: [ { urls: ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'], }, ], iceCandidatePoolSize: 10, }; export default function App() { const [user, setUser] = useState(null); const [callId, setCallId] = useState(''); const [inputCallId, setInputCallId] = useState(''); const [status, setStatus] = useState('idle'); // idle, calling, connected const [isMicOn, setIsMicOn] = useState(true); const [isCameraOn, setIsCameraOn] = useState(true); const pc = useRef(new RTCPeerConnection(servers)); const localVideoRef = useRef(null); const remoteVideoRef = useRef(null); const localStream = useRef(null); // 1. Inicialización de Autenticación useEffect(() => { const initAuth = async () => { try { await signInAnonymously(auth); } catch (error) { console.error("Auth Error:", error); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, setUser); return () => unsubscribe(); }, []); // 2. Configuración de Medios Locales useEffect(() => { const startWebcam = async () => { try { localStream.current = await navigator.mediaDevices.getUserMedia({ video: true, audio: true, }); if (localVideoRef.current) { localVideoRef.current.srcObject = localStream.current; } // Añadir tracks al PeerConnection localStream.current.getTracks().forEach((track) => { pc.current.addTrack(track, localStream.current); }); } catch (e) { console.error("Error accediendo a periféricos:", e); } }; startWebcam(); // Listener para tracks remotos pc.current.ontrack = (event) => { if (remoteVideoRef.current) { remoteVideoRef.current.srcObject = event.streams[0]; setStatus('connected'); } }; return () => { localStream.current?.getTracks().forEach(t => t.stop()); }; }, []); // Función para crear una llamada (Generar Oferta) const createCall = async () => { if (!user) return; setStatus('calling'); const callDoc = doc(collection(db, 'artifacts', appId, 'public', 'data', 'calls')); const offerCandidates = collection(callDoc, 'offerCandidates'); const answerCandidates = collection(callDoc, 'answerCandidates'); setCallId(callDoc.id); pc.current.onicecandidate = (event) => { if (event.candidate) { addDoc(offerCandidates, event.candidate.toJSON()); } }; const offerDescription = await pc.current.createOffer(); await pc.current.setLocalDescription(offerDescription); const offer = { sdp: offerDescription.sdp, type: offerDescription.type, }; await setDoc(callDoc, { offer }); // Escuchar respuesta onSnapshot(callDoc, (snapshot) => { const data = snapshot.data(); if (!pc.current.currentRemoteDescription && data?.answer) { const answerDescription = new RTCSessionDescription(data.answer); pc.current.setRemoteDescription(answerDescription); } }); // Escuchar candidatos remotos onSnapshot(answerCandidates, (snapshot) => { snapshot.docChanges().forEach((change) => { if (change.type === 'added') { const data = change.doc.data(); pc.current.addIceCandidate(new RTCIceCandidate(data)); } }); }); }; // Función para unirse a una llamada (Responder Oferta) const joinCall = async () => { if (!user || !inputCallId) return; setStatus('calling'); const callDoc = doc(db, 'artifacts', appId, 'public', 'data', 'calls', inputCallId); const answerCandidates = collection(callDoc, 'answerCandidates'); const offerCandidates = collection(callDoc, 'offerCandidates'); pc.current.onicecandidate = (event) => { if (event.candidate) { addDoc(answerCandidates, event.candidate.toJSON()); } }; const callData = (await getDoc(callDoc)).data(); if (!callData) { alert("ID de llamada no encontrado"); setStatus('idle'); return; } const offerDescription = callData.offer; await pc.current.setRemoteDescription(new RTCSessionDescription(offerDescription)); const answerDescription = await pc.current.createAnswer(); await pc.current.setLocalDescription(answerDescription); const answer = { type: answerDescription.type, sdp: answerDescription.sdp, }; await updateDoc(callDoc, { answer }); onSnapshot(offerCandidates, (snapshot) => { snapshot.docChanges().forEach((change) => { if (change.type === 'added') { const data = change.doc.data(); pc.current.addIceCandidate(new RTCIceCandidate(data)); } }); }); }; const toggleMic = () => { if (localStream.current) { const audioTrack = localStream.current.getAudioTracks()[0]; audioTrack.enabled = !audioTrack.enabled; setIsMicOn(audioTrack.enabled); } }; const toggleCamera = () => { if (localStream.current) { const videoTrack = localStream.current.getVideoTracks()[0]; videoTrack.enabled = !videoTrack.enabled; setIsCameraOn(videoTrack.enabled); } }; const copyId = () => { document.execCommand('copy'); // En un entorno real usaríamos navigator.clipboard, pero seguimos la instrucción del sistema const el = document.createElement('textarea'); el.value = callId; document.body.appendChild(el); el.select(); document.execCommand('copy'); document.body.removeChild(el); }; return (
Conexión directa P2P segura
{status === 'calling' ? 'Conectando...' : 'Esperando conexión remota...'}
Tu ID de Llamada
{callId}
Comparte este ID con la otra persona para que se una.