Estado
Aceptada — implementada en s79, commit fc83bf5 (22-05-2026).
Contexto
El workspace interno del equipo (Edu/Dani/Txell) necesitaba videollamadas integradas. La solución anterior usaba un <iframe> embebido apuntando a meet.jit.si (servicio público gratuito de Jitsi).
Problema detectado en s79
Durante las pruebas se descubrió que meet.jit.si público impone dos fricciones no suprimibles vía URL params:
- Pantalla prejoin “Join Meeting”: aunque
prejoinPageEnabled=falsefunciona para algunos usuarios, el servidor público forzaba la pantalla en ciertos contextos. - “Sign in as moderator”: el primer participante en una sala vacía recibe una pantalla de login para reclamar el rol de moderador. Es un mecanismo anti-spam de Jitsi público, no controlable por parámetros de URL.
El componente JitsiMeetingModal.tsx ya tenía documentado en su código que esta segunda pantalla era una “restricción no controlable de meet.jit.si público”. La alternativa de self-hostearse Jitsi requería un servidor dedicado (~5 €/mes + mantenimiento Docker).
Opciones evaluadas
| Opción | Coste | Fricción UX | Privacidad | Complejidad |
|---|---|---|---|---|
| Jitsi self-hosted (Hetzner) | ~5€/mes + ops | Mínima | Alta | Media |
| Daily.co / Whereby | ~15€/mes | Mínima | Media | Baja |
| WebRTC nativo browser + signaling propio | 0€ | Cero | Máxima | Media |
| Jitsi público (status quo) | 0€ | Alta (screens no suprimibles) | Media | — |
Decisión
Migrar a WebRTC nativo del browser con un endpoint de señalización mínimo sobre D1.
El vídeo va directo peer-to-peer (browser ↔ browser) usando:
- STUN público de Google (
stun.l.google.com:19302) para el ICE hole-punching. - D1 como canal de señalización efímero (TTL 60s, polling 1.5s).
- Sin TURN en primera iteración (LAN/WiFi interno del equipo funciona sin NAT estricto).
Consecuencias
Positivas
- Cero fricción UX: el usuario entra directo a la sala con cámara/mic activos, sin pantallas intermedias.
- Cero coste: STUN Google es gratuito; D1 gestiona bytes mínimos (~30 rows/sesión × 60s TTL).
- Cero infra nueva: no hay servidor de medios, no hay Docker, no hay servicio a mantener.
- Privacidad máxima: el vídeo nunca pasa por Jitsi, Cloudflare ni ningún tercero.
- Control total: la UX (grid de vídeos, controles, drag/resize modal) es código propio.
Negativas / riesgos
- NAT estricto: sin TURN server, peers en redes corporativas o CGNAT pueden no conectar. Mitigación: añadir Cloudflare TURN (tier gratuito) si se detecta el problema.
- Sin grabación server-side: por diseño (privacidad). Si se necesita grabación futura, requiere un enfoque diferente.
- Polling latency: hasta ~3s de latencia en la señalización inicial. Aceptable para el caso de uso (3 personas del equipo).
- Mesh a 3 peers: solución válida solo para grupos pequeños. No escala a >5-6 peers sin SFU.
- Mantenimiento propio: si hay bugs en WebRTC, el equipo debe resolverlos (vs. Jitsi como producto maduro).
Implementación
Ver [[feature—workspace—webrtc-peer-to-peer]] para la arquitectura técnica completa.
Componentes creados:
functions/api/webrtc/signal.ts— Endpoint de señalización (CF Pages Function)migrations/0035_webrtc_signaling.sql— Tabla D1 con TTL inlinesrc/components/widgets/MeetingRoom.tsx— Componente React mesh WebRTC
Eliminado:
src/components/widgets/JitsiMeetingModal.tsx(291 LOC)
Trabajo futuro pendiente
- Evaluar TURN de Cloudflare si algún peer reporta fallos de conexión.
- Monitorizar tasa de ICE failures en producción.
- Considerar WebSocket para signaling si el polling 1.5s resulta demasiado lento.
Véase también
- [[feature—workspace—webrtc-peer-to-peer]]
- [[entity—workspace—endpoint—webrtc-signal]]
- [[entity—workspace—table—webrtc-signaling]]
- [[workspace—que-es-workspace]]