Tutorial #4: Entendiendo WebRTC - La Tecnología Detrás del Video Chat
Tutorial #4: Entendiendo WebRTC - La Tecnología Detrás del Video Chat
🎯 Introducción
En esta lección, vamos a entender qué es WebRTC y cómo funciona la magia detrás de las videollamadas en el navegador.
WebRTC te permite capturar tu micrófono y cámara directamente desde el navegador, sin instalar plugins ni software adicional. ¡Todo funciona directamente desde tu navegador!
🌐 ¿Qué es WebRTC?
WebRTC (Web Real-Time Communication) es una tecnología que permite comunicación en tiempo real entre navegadores sin necesidad de servidores intermedios para el flujo de datos.
Características Principales
| Característica | Descripción |
|---|---|
| 📹 Video/Audio | Captura y transmisión en tiempo real |
| 📁 Compartir Archivos | Transferencia P2P de datos |
| 🖥️ Compartir Pantalla | Transmisión de la pantalla del usuario |
| 🔒 Seguro | Cifrado obligatorio (DTLS/SRTP) |
| 🌍 Multiplataforma | Funciona en todos los navegadores modernos |
| 🆓 Gratuito | Open source y sin licencias |
🏗️ Arquitectura WebRTC
1. Comunicación P2P (Peer-to-Peer)
En WebRTC, la comunicación es directa entre usuarios:
[Usuario A] ←────────── P2P ──────────→ [Usuario B]
↑ ↑
└───────── Servidor de Señalización ────┘
(Solo para establecer conexión)2. Servidor de Señalización
¿Para qué sirve?
Intercambiar metadatos iniciales
Coordinar la conexión
No maneja el flujo de datos
Ejemplo en nuestro proyecto:
// El servidor solo intercambia información { type: 'offer', sdp: 'v=0...', // Metadatos de la sesión roomId: 'sala1' }
🔄 Flujo de Comunicación WebRTC
Paso a Paso
1. Usuario A → Crea Oferta (Offer) ├── Captura medios locales ├── Crea objeto RTCPeerConnection └── Genera SDP (Session Description Protocol) 2. Usuario A → Envía Oferta al Servidor └── Servidor de señalización guarda la oferta 3. Usuario B → Recibe Oferta ├── Crea objeto RTCPeerConnection └── Genera Respuesta (Answer) 4. Usuario B → Envía Respuesta al Servidor └── Servidor entrega la respuesta a Usuario A 5. Intercambio ICE Candidates ├── Descubrimiento de IPs públicas ├── Intercambio de puertos └── Negociación de red 6. ✅ ¡Conexión P2P Establecida! └── Los datos fluyen directamente entre usuarios
🎥 Capturando Medios con WebRTC
API de Medios
// Capturar cámara y micrófono const stream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user' }, audio: { echoCancellation: true, noiseSuppression: true } }); // Mostrar en video local const videoElement = document.getElementById('localVideo'); videoElement.srcObject = stream;
Compartir Pantalla
// Capturar pantalla const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: 'always' }, audio: true }); // Reemplazar track de video const sender = pc.getSenders().find(s => s.track.kind === 'video'); sender.replaceTrack(screenStream.getVideoTracks()[0]);
🖥️ El Proceso de Señalización
1. Crear Oferta (Offer)
// Usuario A - Crear oferta const peerConnection = new RTCPeerConnection(config); // Agregar tracks locales localStream.getTracks().forEach(track => { peerConnection.addTrack(track, localStream); }); // Crear oferta const offer = await peerConnection.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true }); await peerConnection.setLocalDescription(offer); // Enviar al servidor await fetch('/api/offer', { method: 'POST', body: JSON.stringify({ offer, targetUser: 'usuarioB' }) });
2. Recibir Oferta y Crear Respuesta (Answer)
// Usuario B - Recibir oferta const data = await response.json(); const offer = new RTCSessionDescription(data.offer); await peerConnection.setRemoteDescription(offer); // Crear respuesta const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); // Enviar respuesta await fetch('/api/answer', { method: 'POST', body: JSON.stringify({ answer, targetUser: 'usuarioA' }) });
3. Intercambio de ICE Candidates
// Escuchar candidatos ICE peerConnection.onicecandidate = (event) => { if (event.candidate) { // Enviar candidato al otro usuario sendCandidate(event.candidate); } }; // Recibir candidatos del otro usuario function receiveCandidate(candidate) { peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); }
🌍 STUN y TURN Servers
¿Qué son?
| Servidor | Función | ¿Cuándo se usa? |
|---|---|---|
| STUN | Descubre IP pública | Siempre (primera opción) |
| TURN | Retransmite datos | Cuando STUN falla |
Diagrama de Funcionamiento
┌─────────────┐
│ Usuario A │
│ 192.168.1.5 │
└──────┬──────┘
│ ¿Cuál es mi IP pública?
↓
┌─────────────┐
│ STUN Server │ ← Devuelve: 201.34.56.78:54321
│ stun:google │
└─────────────┘
│
↓
┌─────────────┐
│ Usuario B │ ← Se conecta a 201.34.56.78:54321
│ 192.168.1.8 │
└─────────────┘
Si falla → Usar TURN Server (relay)Configuración en Código
const config = { iceServers: [ // STUN Server (gratuito) { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, // TURN Server (pagado o self-hosted) { urls: 'turn:turn.miservidor.com:3478', username: 'usuario', credential: 'contraseña' } ], iceCandidatePoolSize: 10 };
¿Cuándo necesitas TURN?
Estadísticas aproximadas:
70-80% de usuarios: Solo necesitan STUN
15-20% de usuarios: Necesitan TURN (firewalls restrictivos)
5-10% de usuarios: No pueden conectar (usar fallback)
🎮 Aplicaciones de WebRTC
1. Videollamadas (WhatsApp, Google Meet, Zoom)
// Ejemplo simplificado class VideoCall { async startCall() { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); // ... establecer conexión P2P } }
2. Juegos en la Nube (Google Stadia)
WebRTC permite:
Bajo latencia (menos de 100ms)
Transmisión de video 4K
Entrada de usuario en tiempo real
3. Compartir Archivos (P2P)
// Compartir archivos sin servidor const dataChannel = peerConnection.createDataChannel('fileTransfer'); dataChannel.onopen = () => { // Enviar archivo en chunks const chunkSize = 16384; // ... enviar datos }; dataChannel.onmessage = (event) => { // Recibir archivo // ... reconstruir archivo };
4. Ver Películas en Grupo
// Sincronizar reproducción dataChannel.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'play') { videoElement.currentTime = data.timestamp; videoElement.play(); } };
🚀 Empresas que Usan WebRTC
| Empresa | Uso |
|---|---|
| Google Meet | Videoconferencias |
| Llamadas de voz/video | |
| Facebook Messenger | Llamadas |
| Discord | Video llamadas y streaming |
| Slack | Llamadas en equipo |
| Amazon Chime | Videoconferencias |
| Google Stadia | Streaming de juegos |
🔐 Seguridad en WebRTC
Cifrado Obligatorio
WebRTC siempre usa cifrado:
DTLS (Datagram Transport Layer Security) para datos
SRTP (Secure Real-time Transport Protocol) para audio/video
¿Qué significa esto?
// Todo está cifrado automáticamente // No necesitas configurar nada adicional // 1. Las claves se negocian automáticamente // 2. El tráfico está encriptado // 3. Nadie puede interceptar las comunicaciones
📊 Comparación WebRTC vs Otras Tecnologías
| Característica | WebRTC | WebSockets | HTTP |
|---|---|---|---|
| Tipo | P2P | Cliente-Servidor | Cliente-Servidor |
| Latencia | Muy baja | Baja | Alta |
| Ancho de banda | Optimizado | Depende | Depende |
| Servidor | Solo señalización | Siempre | Siempre |
| Video/Audio | ✅ Nativo | ❌ No | ❌ No |
| Cifrado | ✅ Siempre | Opcional | Opcional |
| Coste | Bajo (P2P) | Alto (servidor) | Alto (servidor) |
🛠️ Nuestra Implementación
Estructura de WebRTC en Nuestro Proyecto
📁 vchat/
├── 📁 assets/js/
│ ├── 📄 webrtc.js ← Lógica principal de WebRTC
│ └── 📄 app.js ← Interfaz de usuario
├── 📁 core/classes/
│ └── 📄 WebRTC.php ← Señalización en servidor
└── 📁 websocket/
└── 📄 server.js ← Señalización en tiempo realCódigo Base de WebRTC
// assets/js/webrtc.js class WebRTCManager { constructor() { this.peerConnection = null; this.localStream = null; this.remoteStream = new MediaStream(); this.isCalling = false; } async initLocalStream() { try { this.localStream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user' }, audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); return this.localStream; } catch (error) { console.error('Error al acceder a los medios:', error); throw error; } } async createPeerConnection(config) { this.peerConnection = new RTCPeerConnection(config || { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } ] }); // Manejar tracks remotos this.peerConnection.ontrack = (event) => { this.remoteStream.addTrack(event.track); // Actualizar UI con video remoto }; // Manejar ICE candidates this.peerConnection.onicecandidate = (event) => { if (event.candidate) { this.sendCandidate(event.candidate); } }; // Manejar estado de conexión this.peerConnection.onconnectionstatechange = () => { const state = this.peerConnection.connectionState; console.log('Estado de conexión:', state); this.handleConnectionState(state); }; return this.peerConnection; } async createOffer(targetUserId) { if (!this.peerConnection) { await this.createPeerConnection(); } // Agregar tracks locales this.localStream.getTracks().forEach(track => { this.peerConnection.addTrack(track, this.localStream); }); const offer = await this.peerConnection.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true }); await this.peerConnection.setLocalDescription(offer); return { type: 'offer', sdp: offer, targetUserId: targetUserId }; } async handleOffer(offer) { await this.peerConnection.setRemoteDescription( new RTCSessionDescription(offer) ); const answer = await this.peerConnection.createAnswer(); await this.peerConnection.setLocalDescription(answer); return { type: 'answer', sdp: answer }; } async handleAnswer(answer) { await this.peerConnection.setRemoteDescription( new RTCSessionDescription(answer) ); } async addCandidate(candidate) { try { await this.peerConnection.addIceCandidate( new RTCIceCandidate(candidate) ); } catch (error) { console.error('Error al agregar candidate:', error); } } sendCandidate(candidate) { // Enviar candidate a través del servidor de señalización window.ws.send(JSON.stringify({ type: 'candidate', candidate: candidate })); } handleConnectionState(state) { switch(state) { case 'connected': console.log('✅ Conexión P2P establecida'); break; case 'disconnected': console.log('⚠️ Conexión perdida'); this.handleDisconnect(); break; case 'failed': console.log('❌ Conexión fallida'); this.handleFailure(); break; } } hangUp() { if (this.peerConnection) { this.peerConnection.close(); this.peerConnection = null; } if (this.localStream) { this.localStream.getTracks().forEach(track => track.stop()); this.localStream = null; } this.remoteStream = new MediaStream(); this.isCalling = false; } handleDisconnect() { // Intentar reconectar console.log('Intentando reconectar...'); } handleFailure() { // Fallback: usar TURN server console.log('Usando TURN server...'); } }
📝 Resumen de Conceptos Clave
| Término | Significado |
|---|---|
| P2P | Conexión directa entre usuarios |
| STUN | Descubre IP pública |
| TURN | Relay cuando P2P falla |
| SDP | Metadatos de la sesión |
| ICE | Framework para establecer conexión |
| Signalización | Intercambio de metadatos iniciales |
| Codec | Algoritmo de compresión (VP8, H.264) |
🎯 Lo que Aprendimos
✅ Qué es WebRTC y para qué sirve
✅ Cómo funciona la comunicación P2P
✅ Qué son STUN y TURN servers
✅ Cómo capturar medios del navegador
✅ El proceso de señalización paso a paso
✅ Aplicaciones reales de WebRTC
🚀 Próximo Paso
En la siguiente lección, vamos a:
Configurar la base de datos
Crear el sistema de autenticación
Implementar el servidor de señalización con WebSockets
¡Excelente trabajo! Ahora entiendes la tecnología que hace posible nuestro video chat
Comentarios
Publicar un comentario