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ísticaDescripción
📹 Video/AudioCaptura y transmisión en tiempo real
📁 Compartir ArchivosTransferencia P2P de datos
🖥️ Compartir PantallaTransmisión de la pantalla del usuario
🔒 SeguroCifrado obligatorio (DTLS/SRTP)
🌍 MultiplataformaFunciona en todos los navegadores modernos
🆓 GratuitoOpen source y sin licencias

🏗️ Arquitectura WebRTC

1. Comunicación P2P (Peer-to-Peer)

En WebRTC, la comunicación es directa entre usuarios:

text
[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:

javascript
// 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

text
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

javascript
// 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

javascript
// 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)

javascript
// 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)

javascript
// 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

javascript
// 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?

ServidorFunción¿Cuándo se usa?
STUNDescubre IP públicaSiempre (primera opción)
TURNRetransmite datosCuando STUN falla

Diagrama de Funcionamiento

text
                  ┌─────────────┐
                  │  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

javascript
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)

javascript
// 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)

javascript
// 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

javascript
// 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

EmpresaUso
Google MeetVideoconferencias
WhatsAppLlamadas de voz/video
Facebook MessengerLlamadas
DiscordVideo llamadas y streaming
SlackLlamadas en equipo
Amazon ChimeVideoconferencias
Google StadiaStreaming 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?

javascript
// 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ísticaWebRTCWebSocketsHTTP
TipoP2PCliente-ServidorCliente-Servidor
LatenciaMuy bajaBajaAlta
Ancho de bandaOptimizadoDependeDepende
ServidorSolo señalizaciónSiempreSiempre
Video/Audio✅ Nativo❌ No❌ No
Cifrado✅ SiempreOpcionalOpcional
CosteBajo (P2P)Alto (servidor)Alto (servidor)

🛠️ Nuestra Implementación

Estructura de WebRTC en Nuestro Proyecto

text
📁 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 real

Código Base de WebRTC

javascript
// 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érminoSignificado
P2PConexión directa entre usuarios
STUNDescubre IP pública
TURNRelay cuando P2P falla
SDPMetadatos de la sesión
ICEFramework para establecer conexión
SignalizaciónIntercambio de metadatos iniciales
CodecAlgoritmo de compresión (VP8, H.264)

🎯 Lo que Aprendimos

  1. Qué es WebRTC y para qué sirve

  2. Cómo funciona la comunicación P2P

  3. Qué son STUN y TURN servers

  4. Cómo capturar medios del navegador

  5. El proceso de señalización paso a paso

  6. 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

Entradas más populares de este blog

Tutorial Completo: Chat de Video en Vivo con PHP + WebRTC