CreaRack-SL

Videollamadas WebRTC peer-to-peer en el Workspace (reemplazo de Jitsi)

Estado: Activo en producción · Introducido en s79 (22-05-2026) · Refactor popup en s80 (22-05-2026, PR#68)


Resumen

Sistema de videollamadas directo browser-a-browser (WebRTC P2P) para el equipo CreaRack. Reemplaza el stack anterior basado en JitsiMeetingModal + servidor Docker externo. Sin servidor de medios, sin Docker, sin Jitsi: el video va directo entre browsers usando únicamente Cloudflare D1 como canal de señalización.


Arquitectura

Peer A (TeamChat)
    |
    | window.open('/reunion?author=Edu&target=Todos')
    v
[Popup /reunion]                    [Popup /reunion]
  MeetingRoomShell                    MeetingRoomShell
    |                                     |
  MeetingRoom ←── /api/webrtc/signal ──→ MeetingRoom
  (browser A)       (D1 polling)          (browser B)
         \                              /
          ──── RTCPeerConnection ────────
                 (WebRTC directo)

Señalización (D1)

  • Tabla webrtc_signaling en Cloudflare D1.
  • Endpoint POST /api/webrtc/signal (Pages Function en functions/api/webrtc/signal.ts): recibe mensajes JSON, los escribe en D1 y los lee por polling.
  • Polling cada ~1.5 s por peer. Mensaje TTL: los registros se limpian tras ser leídos.
  • Tipos de mensaje: peer_joined, peer_left, offer, answer, ice-candidate.
  • El peer con ID lexicográficamente menor actúa como initiator y envía el offer.

Componentes

MeetingRoom.tsx + MeetingRoomShell.tsx + /reunion.astro

Componente React (src/components/widgets/MeetingRoom.tsx) que reemplaza a JitsiMeetingModal.tsx (eliminado, 291 LOC → 0).

Modo de uso (refactor s80 · 22-05-2026): la reunión vive en una ventana popup propia del navegador (/reunion, abierta con window.open(..., 'popup=yes,...') desde el botón Video del TeamChat). Antes era un modal flotante dentro del WS, pero al navegar entre páginas Astro se desmontaba y mataba la sesión WebRTC. El popup es un proceso de navegador independiente: sobrevive a cualquier navegación, se puede arrastrar al segundo monitor y cerrarlo cuelga la reunión sin tocar el WS. Ver [[decision—20260522—meetingroom-popup-window]].

ArchivoRol
src/pages/reunion.astroRuta standalone, layout minimal (AppLayout sin Sidebar/BottomNav), html/body 100vh + overflow:hidden
src/components/widgets/MeetingRoomShell.tsxLee author/target/silent de query params, monta MeetingRoom, onClose → window.close()
src/components/widgets/MeetingRoom.tsxLógica WebRTC + UI ocupando 100% del viewport del popup
src/components/widgets/TeamChat.tsxBotón Video llama a openMeetingPopup(author, target, silent) que hace window.open(...) con name 'crearack-meeting' (singleton)

Responsabilidades de MeetingRoom:

  • Gestiona el array de RTCPeerConnection (una por peer remoto).
  • Implementa el bucle de polling de signaling.
  • Renderiza el grid de vídeo (local + remotos, layout automático CSS Grid).
  • Controles: mute micrófono, mute cámara, screen share, ajustes dispositivos, hang up.
  • beforeunload + pagehide envían peer_left via navigator.sendBeacon para que los otros peers detecten la salida inmediatamente cuando se cierra el popup desde la X del SO.

/api/webrtc/signal (Pages Function)

functions/api/webrtc/signal.ts
  • POST: inserta un mensaje de señalización (offer, answer, ice-candidate, peer_joined, peer_left).
  • GET ?room=<id>&for_peer=<id>: devuelve y borra los mensajes pendientes para ese peer.
  • Sala fija: team-meeting (no multi-sala por ahora).

Tabla D1: webrtc_signaling

Migración: migrations/0035_webrtc_signaling.sql. Columnas: id, room, from_peer, from_name, to_peer (null = broadcast), kind, payload (JSON), created_at.


Flujo de llamada paso a paso

  1. Edu hace click en «📹 Video → Llamar a todos» en TeamChat.
  2. TeamChat llama a openMeetingPopup('Edu', 'Todos', false) → window.open('/reunion?author=Edu&target=Todos', 'crearack-meeting', 'popup=yes,...').
  3. Popup /reunion monta MeetingRoomShell → MeetingRoom.
  4. MeetingRoom llama a getUserMedia, obtiene stream local, genera peerId.
  5. Envía POST /api/webrtc/signal { kind: 'peer_joined', room: 'team-meeting', from_peer: 'eduXXX', ... }.
  6. TeamChat también postea mensaje de aviso al chat con botón «🎥 Unirme».
  7. Dani hace click en «Unirme» → nuevo popup abre /reunion?author=Dani&target=Todos&silent=1.
  8. Polling detecta peer_joined del otro lado. El peer con ID menor envía offer (SDP).
  9. Negociación SDP + ICE candidates via D1. WebRTC directo establecido.
  10. Al colgar (botón o X del SO): sendBeacon → peer_left → el otro peer detecta la salida en el siguiente poll.

Consideraciones de NAT/STUN/TURN

El servidor STUN de Google (stun.l.google.com:19302) solo se usa para el ICE gathering (descubrir IPs públicas). El stream de vídeo va directo entre los browsers sin pasar por Google. Para NAT simétrico estricto (raro en redes de oficina) puede ser necesario un TURN server — pendiente de añadir si se reportan fallos de conexión. Cloudflare ofrece TURN gratuito para volumen bajo.


Tabla de archivos

ArchivoCambio
functions/api/webrtc/signal.ts➕ Añadido (143 LOC)
migrations/0035_webrtc_signaling.sql➕ Añadido (29 LOC)
src/components/widgets/MeetingRoom.tsx➕ Añadido (s79) · ✏️ Refactor a fullscreen popup (s80, PR#68)
src/components/widgets/MeetingRoomShell.tsx➕ Añadido (s80, PR#68)
src/pages/reunion.astro➕ Añadido (s80, PR#68)
src/components/widgets/JitsiMeetingModal.tsx❌ Eliminado (291 LOC)
src/components/widgets/TeamChat.tsx✏️ Switch de import + mensaje de anuncio simplificado · ✏️ s80: usa window.open en lugar de render inline del modal

Véase también

  • [[decision—20260522—webrtc-reemplaza-jitsi]] — ADR que introdujo WebRTC P2P en sustitución de Jitsi
  • [[decision—20260522—meetingroom-popup-window]] — ADR del cambio modal → popup window (s80, PR#68)
  • [[entity—meeting—component—meeting-room]] — Documentación detallada del componente MeetingRoom
  • [[entity—meeting—component—meeting-room-shell]] — Wrapper MeetingRoomShell para la ruta /reunion
  • [[workspace—que-es-workspace]] — Descripción del workspace Astro donde vive la feature