¿Qué es WebRTC?

 Te explico WebRTC (Web Real-Time Communication) y cómo integrarlo con PHP y JavaScript.

¿Qué es WebRTC?

WebRTC es una tecnología que permite comunicación en tiempo real (audio, video y datos) directamente entre navegadores, sin necesidad de plugins ni servidores intermedios para el flujo de datos.

Características clave:

  • P2P (Peer-to-Peer): Los datos viajan directamente entre usuarios

  • Bajo consumo: Optimizado para streaming en tiempo real

  • Seguro: Cifrado obligatorio (DTLS/SRTP)

  • Multiplataforma: Funciona en Chrome, Firefox, Safari, Edge


Arquitectura Básica

text
[Cliente A] ←→ [Servidor de Señalización (PHP)] ←→ [Cliente B]
     ↕ (P2P directo) ↕
[Cliente A] ←─────────── (Media/Data) ───────────→ [Cliente B]

Componentes:

  1. Servidor de señalización (PHP): Intercambia metadatos para establecer la conexión

  2. Clientes (JS): Manejan la lógica de medios y conexión P2P

  3. STUN/TURN Server: Ayuda a atravesar NATs y firewalls


Flujo de Trabajo

1️⃣ Cliente A inicia sesión

javascript
// JavaScript - Crear conexión
const pc = new RTCPeerConnection(config);

// Obtener medios locales
const stream = await navigator.mediaDevices.getUserMedia({ 
  video: true, 
  audio: true 
});

// Crear oferta (SDP - Session Description Protocol)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

// Enviar oferta al servidor PHP
fetch('signal.php', {
  method: 'POST',
  body: JSON.stringify({ 
    type: 'offer', 
    sdp: offer,
    room: 'sala1' 
  })
});

2️⃣ PHP recibe y guarda la oferta

php
// signal.php - Guardar oferta
<?php
session_start();
$data = json_decode(file_get_contents('php://input'), true);
$room = $data['room'] ?? 'default';

// Guardar en sesión o base de datos
if ($data['type'] === 'offer') {
    $_SESSION['offers'][$room] = $data['sdp'];
    echo json_encode(['status' => 'offer_saved']);
}

// Cliente B solicita la oferta
if ($data['type'] === 'get_offer') {
    $offer = $_SESSION['offers'][$room] ?? null;
    echo json_encode(['offer' => $offer]);
}
?>

3️⃣ Cliente B recibe la oferta y responde

javascript
// Cliente B - Obtener oferta
const response = await fetch('signal.php?action=get_offer&room=sala1');
const data = await response.json();

if (data.offer) {
    const pc = new RTCPeerConnection(config);
    
    // Establecer oferta remota
    await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
    
    // Crear respuesta (answer)
    const answer = await pc.createAnswer();
    await pc.setLocalDescription(answer);
    
    // Enviar respuesta al servidor
    fetch('signal.php', {
        method: 'POST',
        body: JSON.stringify({ 
            type: 'answer', 
            sdp: answer,
            room: 'sala1' 
        })
    });
}

4️⃣ PHP entrega la respuesta al Cliente A

php
// signal.php - Manejar answer
if ($data['type'] === 'answer') {
    $_SESSION['answers'][$room] = $data['sdp'];
}

// Cliente A solicita la respuesta
if ($data['type'] === 'get_answer') {
    $answer = $_SESSION['answers'][$room] ?? null;
    echo json_encode(['answer' => $answer]);
}

5️⃣ Cliente A completa la conexión

javascript
// Cliente A - Obtener respuesta
const resp = await fetch('signal.php?action=get_answer&room=sala1');
const ansData = await resp.json();

if (ansData.answer) {
    await pc.setRemoteDescription(
        new RTCSessionDescription(ansData.answer)
    );
    // ¡Conexión establecida!
}

Servidor de Señalización con PHP (Completo)

php
// signal.php - Versión robusta con MySQL
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');

$pdo = new PDO('mysql:host=localhost;dbname=webrtc', 'user', 'pass');

$action = $_GET['action'] ?? $_POST['action'] ?? '';
$room = $_GET['room'] ?? 'default';

switch($action) {
    case 'offer':
        $sdp = json_decode(file_get_contents('php://input'), true)['sdp'];
        // Guardar oferta
        $stmt = $pdo->prepare("INSERT INTO sessions (room, offer, created_at) 
                               VALUES (?, ?, NOW()) 
                               ON DUPLICATE KEY UPDATE offer = ?");
        $stmt->execute([$room, $sdp, $sdp]);
        echo json_encode(['status' => 'ok']);
        break;
        
    case 'get_offer':
        $stmt = $pdo->prepare("SELECT offer FROM sessions WHERE room = ?");
        $stmt->execute([$room]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        echo json_encode(['offer' => $result['offer'] ?? null]);
        break;
        
    case 'answer':
        $sdp = json_decode(file_get_contents('php://input'), true)['sdp'];
        $stmt = $pdo->prepare("UPDATE sessions SET answer = ? WHERE room = ?");
        $stmt->execute([$sdp, $room]);
        echo json_encode(['status' => 'ok']);
        break;
        
    case 'get_answer':
        $stmt = $pdo->prepare("SELECT answer FROM sessions WHERE room = ?");
        $stmt->execute([$room]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        echo json_encode(['answer' => $result['answer'] ?? null]);
        break;
}
?>

Configuración STUN/TURN

javascript
// Configuración para producción
const config = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },  // STUN gratuito
        { 
            urls: 'turn:turn.myserver.com:3478',
            username: 'user',
            credential: 'pass'
        }
    ],
    iceCandidatePoolSize: 10
};

Ejemplo Completo HTML + JS

html
<!DOCTYPE html>
<html>
<body>
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
    <button onclick="startCall()">Iniciar Llamada</button>
    <button onclick="joinCall()">Unirse</button>

    <script>
        let pc, localStream;
        const room = 'sala1';
        const serverUrl = 'signal.php';

        async function startCall() {
            localStream = await navigator.mediaDevices.getUserMedia({ 
                video: true, audio: true 
            });
            document.getElementById('localVideo').srcObject = localStream;

            pc = new RTCPeerConnection(config);
            localStream.getTracks().forEach(track => 
                pc.addTrack(track, localStream)
            );

            pc.ontrack = (event) => {
                document.getElementById('remoteVideo').srcObject = event.streams[0];
            };

            const offer = await pc.createOffer();
            await pc.setLocalDescription(offer);

            // Enviar oferta via PHP
            await fetch(serverUrl, {
                method: 'POST',
                body: JSON.stringify({ action: 'offer', room, sdp: offer })
            });

            // Polling para obtener respuesta
            checkForAnswer();
        }

        async function checkForAnswer() {
            const resp = await fetch(`${serverUrl}?action=get_answer&room=${room}`);
            const data = await resp.json();
            
            if (data.answer) {
                await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
                return;
            }
            
            setTimeout(checkForAnswer, 1000); // Polling cada 1s
        }

        async function joinCall() {
            localStream = await navigator.mediaDevices.getUserMedia({ 
                video: true, audio: true 
            });
            document.getElementById('localVideo').srcObject = localStream;

            pc = new RTCPeerConnection(config);
            localStream.getTracks().forEach(track => 
                pc.addTrack(track, localStream)
            );

            pc.ontrack = (event) => {
                document.getElementById('remoteVideo').srcObject = event.streams[0];
            };

            // Obtener oferta
            const resp = await fetch(`${serverUrl}?action=get_offer&room=${room}`);
            const data = await resp.json();

            if (data.offer) {
                await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
                const answer = await pc.createAnswer();
                await pc.setLocalDescription(answer);

                await fetch(serverUrl, {
                    method: 'POST',
                    body: JSON.stringify({ action: 'answer', room, sdp: answer })
                });
            }
        }
    </script>
</body>
</html>

Conceptos Clave

TérminoSignificado
SDPSession Description Protocol - Metadatos de la sesión
ICEInteractive Connectivity Establishment - Descubre cómo conectar peers
STUNSession Traversal Utilities for NAT - Descubre IP pública
TURNTraversal Using Relays around NAT - Relay cuando P2P falla
SignalingIntercambio de metadatos para establecer conexión

Mejores Prácticas

  1. Usa WebSockets en lugar de HTTP polling para señalización en tiempo real

  2. Implementa autenticación en el servidor PHP

  3. Configura TURN server para casos donde P2P falle

  4. Maneja desconexiones: Implementa reconnection logic

  5. Cifra los datos: Usa RTCDataChannel con encrypted: true

Limitaciones

  • ⚠️ PHP no maneja la conexión P2P, solo señalización

  • ⚠️ Requiere HTTPS (o localhost) para funcionar

  • ⚠️ Depende del navegador para la implementación

  • ⚠️ No apto para muchos usuarios (es P2P, no broadcast)


¿Necesitas profundizar en alguna parte específica (como manejo de canales de datos, salas múltiples, o integración con WebSockets)?

Comentarios

Entradas más populares de este blog

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