From 89446e701ff74097e88a918cc864a1d03e439b68 Mon Sep 17 00:00:00 2001 From: Cesar Mendivil Date: Sat, 14 Mar 2026 16:16:03 -0700 Subject: [PATCH] Adapted preview from WebRTC --- WHIP_UI_INTEGRATION_PROMPT.md | 424 +++++++++++++++++- src/utils/restreamer.js | 2 + src/views/Edit/Wizard/Sources/InternalWHIP.js | 227 ++++++++-- .../Edit/Wizard/Sources/InternalWHIP.test.js | 74 ++- src/views/Main/index.js | 300 ++++++++++++- 5 files changed, 973 insertions(+), 54 deletions(-) diff --git a/WHIP_UI_INTEGRATION_PROMPT.md b/WHIP_UI_INTEGRATION_PROMPT.md index 3d3b8d3..a071b81 100644 --- a/WHIP_UI_INTEGRATION_PROMPT.md +++ b/WHIP_UI_INTEGRATION_PROMPT.md @@ -108,9 +108,13 @@ Ejemplo visual: #### 1.6 Configuración WHIP (settings) Sección colapsable o en la misma pantalla de configuración del Core: -- Toggle **Habilitar WHIP Server** → modifica `config.whip.enable` via `PATCH /api/v3/config` -- Campo **Puerto** → modifica `config.whip.address` +- Toggle **Habilitar WHIP Server** → modifica `config.whip.enable` +- Campo **Puerto WHIP** → modifica `config.whip.address` (ej. `8555`) - Campo **Token** → modifica `config.whip.token` +- Campo **Puerto relay RTSP interno** → modifica `config.whip.rtsp_address` (ej. `8554`) + - Mostrar tooltip: *"Habilita el relay RTSP para que múltiples procesos FFmpeg puedan consumir el mismo stream WHIP simultáneamente. Dejar vacío para desactivar."* + - Si está vacío: el relay RTSP está desactivado; solo un proceso puede leer el stream con `{whip}`. + - Si está configurado: múltiples procesos pueden usar `{whip-rtsp}` como input. --- @@ -267,7 +271,8 @@ Leer configuración del servidor WHIP para el panel de settings. "whip": { "enable": true, "address": ":8555", - "token": "" + "token": "", + "rtsp_address": ":8554" } } } @@ -392,6 +397,104 @@ El proceso comenzará a recibir video cuando OBS empiece a transmitir. --- +## Parte 5 — Recuperación de estado tras reinicio del Core + +Cuando el Core se reinicia, la UI debe re-leer el estado WHIP desde la API. La UI **no debe cachear** los valores localmente — siempre debe obtenerlos del Core en el momento de montar el componente. + +### 5.1 Secuencia de inicialización del componente WHIP + +Al montar cualquier componente/página que muestre datos WHIP, ejecutar en este orden: + +```js +async function load() { + // 1. Leer config del servidor + const { data } = await api.get('/api/v3/config') + const whip = data.config.whip ?? { enable: false, address: ':8555', token: '' } + + // 2. Extraer puerto sin el prefijo ':' + const port = whip.address.replace(/^:/, '') || '8555' + + // 3. Inicializar estado del formulario + configValues.whip = { + enable: whip.enable, + address: port, // mostrar "8555", no ":8555" + token: whip.token, + rtspAddress: (whip.rtsp_address ?? '').replace(/^:/, ''), // "8554" o "" + } + + // 4. Obtener la URL base de publicación (host público) + const urlInfo = await api.get('/api/v3/whip/url') + state.basePublishUrl = urlInfo.data.base_publish_url // "http://189.x.x.x:8555/whip/" + state.hasToken = urlInfo.data.has_token + + // 5. Listar publishers activos + const channels = await api.get('/api/v3/whip') + state.activeChannels = channels.data // [{ name, published_at }] +} +``` + +### 5.2 Detectar que el Core se reinició + +El Core no emite eventos de reconexión, pero la UI puede detectarlo por: + +- **Polling de `/api/v3/whip`** — si la llamada falla (network error / 502) y luego vuelve a responder, el Core se reinició. Llamar `load()` nuevamente. +- **`updated_at` en `/api/v3/config`** — si cambia respecto al valor previo cacheado, el Core fue reiniciado y la config debe recargarse. + +```js +// Ejemplo: detectar reinicio por cambio en updated_at +let lastUpdatedAt = null + +setInterval(async () => { + try { + const { data } = await api.get('/api/v3/config') + if (lastUpdatedAt && data.updated_at !== lastUpdatedAt) { + await load() // Core reinició, recargar todo + } + lastUpdatedAt = data.updated_at + } catch { + // Core no disponible temporalmente, reintentar en próximo ciclo + } +}, 10000) // cada 10s +``` + +### 5.3 Comportamiento esperado en el panel WHIP Server tras reinicio + +| Estado | Qué hace la UI | +|---|---| +| Core acaba de arrancar, config.json existe | `load()` lee `whip.token = 'heavy666'` del disco — se muestra correctamente ✅ | +| Core arranca sin config.json (primera vez) | `whip` no existe en respuesta → usar defaults `{ enable: false, address: '8555', token: '' }` | +| Core no responde (arrancando) | Mostrar spinner/skeleton, reintentar cada 2s hasta que responda | +| Core responde pero WHIP disabled | Mostrar badge "Disabled", ocultar la URL de publicación | + +### 5.4 Flujo de guardado (`handleSave`) + +```js +async function handleSave() { + // Leer config completa actual para no pisar otros campos + const { data } = await api.get('/api/v3/config') + const fullConfig = data.config + + // Aplicar cambios WHIP (restaurar ':' en address) + fullConfig.whip = { + enable: configValues.whip.enable, + address: ':' + configValues.whip.address, // "8555" → ":8555" + token: configValues.whip.token, + rtsp_address: configValues.whip.rtspAddress ? ':' + configValues.whip.rtspAddress : '', // "" o ":8554" + } + + // Enviar config completa + await api.put('/api/v3/config', fullConfig) + + // Recargar URL pública por si cambió el puerto + const urlInfo = await api.get('/api/v3/whip/url') + state.basePublishUrl = urlInfo.data.base_publish_url +} +``` + +> **Importante**: usar siempre `PUT /api/v3/config` con el objeto completo. El Core no soporta PATCH parcial — si se envía solo `{ whip: {...} }`, los demás campos se resetean a defaults. + +--- + ## Notas técnicas para el desarrollador de la UI 1. **Compatibilidad de OBS**: OBS 30+ soporta WHIP nativo. En Configuración → Transmisión, seleccionar "Servicio: Personalizado" y pegar la URL completa. @@ -407,3 +510,318 @@ El proceso comenzará a recibir video cuando OBS empiece a transmitir. 6. **Puertos**: Por defecto el WHIP server escucha en `:8555` (HTTP, sin TLS). El Core HTTP principal escucha en `:8080`. Son servidores independientes. 7. **Múltiples publishers**: El servidor WHIP soporta múltiples stream keys simultáneos. Cada stream key es independiente y genera su propio relay interno. + +--- + +## Parte 6 — WHEP: reproducción en browser sin FFmpeg (nueva funcionalidad) + +WHEP (WebRTC HTTP Egress Protocol) es el complemento de WHIP. Permite que un browser reciba el stream de OBS directamente con latencia ~300ms, **sin transcoding ni FFmpeg**. El Core actúa como SFU: recibe RTP de OBS y lo reenvía vía DTLS-SRTP a cada suscriptor. + +### 6.1 Cambios en las APIs existentes + +#### `GET /api/v3/whip` — campo nuevo: `subscribers` +```json +[ + { + "name": "b89d39bb-5321-46f3-8d89-54a03150205d", + "published_at": "2026-03-14T20:09:09.87237418Z", + "subscribers": 2 + } +] +``` +Usar `subscribers` para mostrar el contador de viewers en tiempo real en el panel WHIP Server. + +#### `GET /api/v3/whip/:name/url` — campo nuevo: `whep_url` +```json +{ + "publish_url": "http://192.168.1.15:8555/whip/mystream", + "sdp_url": "http://localhost:8555/whip/mystream/sdp", + "stream_key": "mystream", + "whep_url": "http://192.168.1.15:8555/whip/mystream/whep" +} +``` + +#### `GET /api/v3/whip/url` — campo nuevo: `base_whep_url` +```json +{ + "base_publish_url": "http://192.168.1.15:8555/whip/", + "base_sdp_url": "http://localhost:8555/whip/", + "base_whep_url": "http://192.168.1.15:8555/whip/", + "has_token": false, + "example_obs_url": "http://192.168.1.15:8555/whip/", + "input_address_template": "{whip:name=}" +} +``` +La `whep_url` completa se construye como: `base_whep_url + stream_key + "/whep"`. + +--- + +### 6.2 Nuevo endpoint WHEP + +#### `POST /whip/:name/whep` — suscribirse al stream (en puerto 8555, **no 8080**) + +El browser envía un SDP offer; el Core responde con SDP answer + ICE candidates. + +> **IMPORTANTE**: Este endpoint vive en el servidor WHIP (puerto `:8555`), **no** en la API REST de Core (`:8080`). Llamarlo directamente desde el browser: +> ``` +> POST http://192.168.1.15:8555/whip/mystream/whep +> ``` + +**Headers de request:** +``` +Content-Type: application/sdp +Authorization: Bearer (solo si has_token = true) +``` + +**Response `201 Created`:** +``` +Content-Type: application/sdp +Location: /whip/mystream/whep/ +Body: +``` + +**Response `404`** si no existe el stream key (canal no creado aún). +**Response `401`** si el token es requerido y no fue enviado. + +#### `DELETE /whip/:name/whep/:subid` — cerrar suscripción + +``` +DELETE http://192.168.1.15:8555/whip/mystream/whep/ +``` +Donde `` es el último segmento del header `Location` devuelto en el POST. + +--- + +### 6.3 Preview embed en el panel WHIP Server + +Agregar un botón **"Ver en vivo"** / **"Preview"** en la tarjeta de cada stream activo. Al hacer clic: + +```js +async function startWHEPPreview(streamKey, whepBaseUrl) { + const pc = new RTCPeerConnection({ iceServers: [] }) + + pc.addTransceiver('video', { direction: 'recvonly' }) + pc.addTransceiver('audio', { direction: 'recvonly' }) + + const offer = await pc.createOffer() + await pc.setLocalDescription(offer) + + const whepUrl = `${whepBaseUrl}${streamKey}/whep` + + const headers = { 'Content-Type': 'application/sdp' } + if (token) headers['Authorization'] = `Bearer ${token}` + + const resp = await fetch(whepUrl, { + method: 'POST', + headers, + body: offer.sdp, + }) + + if (!resp.ok) { + throw new Error(`WHEP error ${resp.status}`) + } + + const locationHeader = resp.headers.get('Location') + const answerSDP = await resp.text() + + await pc.setRemoteDescription({ type: 'answer', sdp: answerSDP }) + + // Mostrar video en el elemento