Librería WebRTC agnóstica a frameworks (TS/JS) para videollamadas personalizables, seguras y ligeras. P2P 1‑a‑1 con cámara, micrófono, compartir pantalla, blur de fondo, moderación y chat.
📖 Manuales internos en
docs/· Roadmap enROADMAP.md
opWebRTC/
├── packages/
│ ├── core/ → @opwebrtc/core · librería TS agnóstica (publicable a npm)
│ └── signaling-server/ → backend WebSocket: señalización + salas + auditoría
└── demo/ → app vanilla TS que consume el core (prueba de agnosticismo)
- core y signaling-server son el producto.
- demo existe para demostrar que el core no depende de ningún framework de UI.
En P2P el servidor no toca la media: el video/audio van cifrados directo entre los dos navegadores (DTLS‑SRTP). El backend solo hace de "celestina" y portero: intercambia SDP/ICE, agrupa peers en salas y registra auditoría de metadatos.
Peer A ──offer/ice──→ [signaling-server] ──offer/ice──→ Peer B
Peer A ←─answer/ice── [signaling-server] ←─answer/ice── Peer B
(luego la media fluye DIRECTA y cifrada entre A y B)
npm install
npm run build # compila @opwebrtc/core (ESM + CJS + tipos)
# en dos terminales:
npm run dev:signaling # ws://localhost:8080
npm run dev:demo # http://localhost:5173Abre el demo en dos pestañas, usa la misma sala y pulsa Unirse en ambas.
Para probar entre dos máquinas distintas necesitas HTTPS/WSS (getUserMedia exige contexto seguro fuera de localhost) y un TURN (ej.
coturn) para atravesar NAT/firewalls.
import { Call } from '@opwebrtc/core';
const call = new Call({
signalingUrl: 'wss://tu-backend/ws',
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
audit: true,
});
call.on('localStream', (s) => localVideo.srcObject = s);
call.on('remoteStream', (s) => remoteVideo.srcObject = s);
call.on('stateChange', (s) => console.log('estado:', s));
call.on('audit', (e) => log(e)); // solo metadatos, nunca contenido
await call.join('sala-123');
call.toggleCamera(false);
call.toggleMic(false);
await call.hangup();El mismo core funciona en React, Vue o vanilla: solo emite MediaStream y eventos.
// Dispositivos
const { cameras, microphones } = await call.getDevices();
await call.switchCamera(cameras[1].deviceId);
await call.switchMicrophone(microphones[0].deviceId);
// Mute
call.toggleCamera(false);
call.toggleMic(false);
// Compartir pantalla (revierte a cámara al terminar)
await call.startScreenShare();
await call.stopScreenShare();
// TURN efímero: credenciales frescas por conexión
new Call({ signalingUrl, iceServersProvider: async () =>
(await fetch('/ice').then(r => r.json())).iceServers });
// Reconexión automática (ICE restart) — integrada, configurable
new Call({ signalingUrl, maxReconnectAttempts: 5 });Módulo aparte para no inflar el core; MediaPipe solo se descarga si lo usas:
import { BackgroundBlur } from '@opwebrtc/core/blur';
const blur = new BackgroundBlur({ blurRadius: 12 });
const blurred = await blur.process(cameraTrack);
await call.replaceOutgoingVideo(blurred);
// ...
blur.stop();infra/ trae un docker-compose.yml + turnserver.conf listos. El
signaling-server firma credenciales efímeras (HMAC) en GET /ice con el mismo
TURN_SECRET. Ver infra/ y .env.example.
- Media cifrada por defecto (DTLS‑SRTP) — propio de WebRTC.
- En producción: WSS (TLS en señalización), autenticación por token de sala, y credenciales TURN efímeras emitidas por el backend (nunca fijas en el front).
Desactivada por defecto. Para exigir token, arranca el servidor con:
AUTH_ENABLED=true AUTH_TOKEN=mi-secreto npm run dev:signalingY pásalo en el cliente:
new Call({ signalingUrl, token: 'mi-secreto' });Si está activada y el token no coincide, el servidor responde unauthorized,
cierra la conexión y el core emite un evento error. Ver
packages/signaling-server/.env.example.
- Auth por token en señalización (parametrizable, off por defecto)
- WSS (TLS en señalización, vía
TLS_CERT/TLS_KEY) - Credenciales TURN efímeras (
GET /ice+ coturn) - Reconexión / ICE restart
- Selección de dispositivos (cámara/mic) en vivo
- Compartir pantalla
- Blur de fondo (módulo opt-in)
- Demo funcional estilo Jitsi (logo overlay, controles, stats)
- Moderación P2P: rol admin + sacar participante (kick)
- Chat de texto por data channel
- CI (GitHub Actions) con 12 tests e2e Playwright
- Migración a SFU para llamadas grupales
- Moderación grupal: mute all (silenciar a todos)
- Control de calidad por capas (simulcast, bitrate)
- Blur en la vista remota: corregido con Insertable Streams (WebCodecs); fallback a canvas en Safari/Firefox