restreamer-ui-v2/WHIP_UI_INTEGRATION_PROMPT.md
Cesar Mendivil 5c586a2aa3 feat: add WHIP-RTSP support for multi-platform streaming
- Introduced WHIP-RTSP as a new source option in Source.js.
- Enhanced Restreamer to handle RTSP relay configuration and address parsing.
- Updated WebRTCRoom to allow selection between direct and RTSP relay modes.
- Implemented WHIPPublications component for managing multiple WHIP destinations.
- Added internal RTSP relay configuration in Settings.js.
- Improved handling of WHEP and WHIP streams, including error logging and state management.
2026-03-15 17:04:54 -07:00

43 KiB

Prompt de integración WHIP para Restreamer UI

Este documento es el brief técnico completo para adaptar la UI de Restreamer de modo que exponga el servidor WHIP del Core de la misma manera que ya expone RTMP Server. Incluye dos casos de uso: WHIP Server (panel de estado + URL para OBS) y WHIP Receive mode (fuente de entrada en un proceso).


Contexto del Core

El Core de datarhei expone las siguientes APIs relevantes:

Config (GET /api/v3/config)

La sección whip dentro de config tiene esta forma:

"whip": {
  "enable": true,
  "address": ":8555",
  "token": ""
}

El campo address puede ser :8555 (solo puerto) o host:8555.

Canales activos (GET /api/v3/whip)

Devuelve la lista de streams que OBS/clientes están publicando en este momento:

[
  {
    "name": "9VGyRCkCVCgj",
    "published_at": "2026-03-14T02:51:36.799550551Z"
  }
]

URL de publicación WHIP (para OBS)

http://<host>:<whip_port>/whip/<stream_key>

Ejemplo: http://192.168.1.15:8555/whip/mistream

URL de relay SDP (para FFmpeg / proceso interno)

http://localhost:<whip_port>/whip/<stream_key>/sdp

Esta es la URL que el Core inyecta automáticamente con {whip} como placeholder de input.

Placeholder de proceso

En el config de un proceso de FFmpeg, el input address puede usar:

{whip:name=<stream_key>}

El Core lo expande a http://localhost:<whip_port>/whip/<stream_key>/sdp y agrega automáticamente -protocol_whitelist file,crypto,http,rtp,udp,tcp en las input options.


Parte 1 — Panel "WHIP Server" (equivalente a "RTMP Server")

Dónde ubicarlo

En la misma sección de configuración/status donde aparece RTMP Server y SRT Server. Añadir una tarjeta o sección llamada WHIP Server.

Comportamiento esperado

1.1 Indicador de estado

  • Leer config.whip.enable de GET /api/v3/config
  • Mostrar badge Enabled (verde) / Disabled (gris)
  • Mostrar el puerto: extraer de config.whip.address (ej. :85558555)

1.2 Campo "Stream URL para OBS"

Construir la URL de publicación a mostrar:

http://<host_público>:<whip_port>/whip/
  • host_público: usar config.host.name[0] si config.host.auto === true, o el primer valor de config.host.name
  • whip_port: extraer de config.whip.address
  • Mostrar un campo de texto read-only con botón Copiar
  • Indicar al usuario que al final de la URL debe agregar su stream key (ej. mistream)

Ejemplo visual:

┌─────────────────────────────────────────────────────┐
│  WHIP Server URL                                    │
│  http://192.168.1.15:8555/whip/          [Copiar]   │
│  Ingresá tu stream key al final de la URL           │
└─────────────────────────────────────────────────────┘

1.3 Campo "Stream Key"

  • Input de texto editable donde el usuario escribe su stream key (ej. mistream, obs-live, etc.)
  • Al escribirla, actualizar dinámicamente la URL completa a copiar: http://192.168.1.15:8555/whip/mistream
  • Botón Copiar URL completa

1.4 Token de autenticación (opcional)

  • Si config.whip.token no está vacío, mostrar un aviso: "Este servidor requiere un token. Agregá ?token=<token> al final de la URL."
  • Opcionalmente, ofrecer un checkbox "Incluir token en la URL" que lo appende automáticamente.

1.5 Tabla de streams activos

  • Hacer polling a GET /api/v3/whip cada 5 segundos
  • Mostrar una tabla con columnas: Stream Key | Publicando desde
  • Si la lista está vacía, mostrar "Sin streams activos"
┌──────────────────┬──────────────────────┐
│ Stream Key       │ Publicando desde      │
├──────────────────┼──────────────────────┤
│ 9VGyRCkCVCgj    │ hace 3 minutos        │
│ mistream         │ hace 12 segundos      │
└──────────────────┴──────────────────────┘

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
  • 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.

Parte 2 — Modo de recepción WHIP ("Receive mode")

Dónde ubicarlo

En el wizard de creación/edición de un proceso (input source selector), junto a las opciones existentes como:

  • Network Source (HLS, RTP, RTSP, etc.)
  • RTMP Source
  • SRT Source
  • WHIP Source ← (nueva opción)

Comportamiento esperado

2.1 Selector de protocolo

En la lista de protocolos de entrada agregar la opción:

WHIP (WebRTC HTTP Ingestion Protocol)

2.2 Formulario de configuración de la fuente WHIP

Al seleccionar WHIP como fuente, mostrar:

Stream Key — campo de texto editable
(Se recomienda usar el ID del proceso por defecto, o dejar que el usuario lo cambie)

URL de publicación para OBS (campo read-only + botón Copiar):

http://<host_público>:<whip_port>/whip/<stream_key>

Actualiza en tiempo real mientras el usuario escribe el stream key.

┌──────────────────────────────────────────────────────┐
│  Protocolo de entrada: WHIP                          │
│                                                      │
│  Stream Key:  [mistream               ]              │
│                                                      │
│  URL para OBS (copiar en OBS → Servicio WHIP):       │
│  http://192.168.1.15:8555/whip/mistream   [Copiar]   │
│                                                      │
│  Estado: ● Esperando publicador...                   │
│           ✓ Transmitiendo  (si hay publisher activo) │
└──────────────────────────────────────────────────────┘

2.3 Indicador de estado en tiempo real

  • Consultar GET /api/v3/whip cada 5 segundos
  • Si name === stream_key aparece en la lista → mostrar "✓ Transmitiendo" (verde)
  • Si no → "● Esperando publicador..." (gris/naranja)

2.4 Address interno del proceso

Cuando el usuario guarda el proceso, el input.address debe configurarse como:

{whip:name=<stream_key>}

El Core lo expande a http://localhost:<whip_port>/whip/<stream_key>/sdp y agrega -protocol_whitelist automáticamente.

Si la UI no soporta el placeholder {whip}, puede usar directamente:

http://localhost:<whip_port>/whip/<stream_key>/sdp

Y agregar en input.options (ANTES de la URL, es decir, al principio del array):

["-protocol_whitelist", "file,crypto,http,rtp,udp,tcp"]

IMPORTANTE: -protocol_whitelist file,crypto,http,rtp,udp,tcp debe estar presente para que FFmpeg pueda abrir los sub-protocolos RTP/UDP anidados dentro de la URL HTTP. Sin esta opción el probe devuelve 0x0 none.

2.5 Probe automático

Cuando el usuario hace clic en "Probe" o "Detectar streams":

  • La UI puede invocar GET /api/v3/process/<id>/probe si el proceso ya existe
  • O construir un payload de probe temporal equivalente
  • El resultado correcto debe ser: Video: h264, yuv420p, 1920x1080, 30fps + Audio: opus, 48000 Hz, stereo

Parte 3 — API endpoints completos de WHIP

3.1 GET /api/v3/whip

Lista todos los publishers activos en este momento.

Response 200:

[
  {
    "name": "mistream",
    "published_at": "2026-03-14T02:51:36.799550551Z"
  }
]

Usar con polling cada 5s para el panel de streams activos y el indicador de estado en Receive mode.


3.2 GET /api/v3/whip/url nuevo

Devuelve la URL base del servidor WHIP y toda la info que la UI necesita para el panel "WHIP Server".

Response 200:

{
  "base_publish_url": "http://192.168.1.15:8555/whip/",
  "base_sdp_url": "http://localhost:8555/whip/",
  "has_token": false,
  "example_obs_url": "http://192.168.1.15:8555/whip/<stream-key>",
  "input_address_template": "{whip:name=<stream-key>}"
}
  • base_publish_url → mostrar en el panel WHIP Server; el usuario agrega su stream key al final
  • has_token → si es true, mostrar aviso de token y el campo para incluirlo en la URL
  • example_obs_url → texto de ayuda con placeholder visual
  • input_address_template → usar como input.address al crear un proceso con WHIP como fuente

3.3 GET /api/v3/whip/:name/url nuevo

Devuelve la URL completa de publicación para un stream key específico. Este es el endpoint principal para generar la URL que el usuario copia en OBS.

Ejemplo: GET /api/v3/whip/mistream/url

Response 200:

{
  "publish_url": "http://192.168.1.15:8555/whip/mistream",
  "sdp_url": "http://localhost:8555/whip/mistream/sdp",
  "stream_key": "mistream"
}
  • publish_url → campo read-only en la UI con botón Copiar. Es exactamente la URL que se pega en OBS → Configuración → Transmisión → Servidor.
  • sdp_url → URL interna para FFmpeg (referencia, no se muestra al usuario en general).
  • stream_key → confirmación del key procesado.

Flujo de uso en la UI:

  1. Usuario escribe el stream key en el campo de texto
  2. UI llama GET /api/v3/whip/<stream_key>/url
  3. UI muestra publish_url en campo read-only con botón Copiar
  4. Usuario copia y pega en OBS

3.4 GET /api/v3/config

Leer configuración del servidor WHIP para el panel de settings.

Campos relevantes en la respuesta:

{
  "config": {
    "host": {
      "name": ["192.168.1.15"],
      "auto": true
    },
    "whip": {
      "enable": true,
      "address": ":8555",
      "token": "",
      "rtsp_address": ":8554"
    }
  }
}

3.5 PATCH /api/v3/config (o PUT)

Modificar configuración WHIP desde el panel de settings.

Body:

{
  "whip": {
    "enable": true,
    "address": ":8555",
    "token": "mi-token-secreto"
  }
}

3.6 POST /api/v3/process

Crear un proceso con WHIP como fuente de entrada.

Body:

{
  "id": "mi-proceso",
  "reference": "mi-proceso",
  "input": [
    {
      "id": "in",
      "address": "{whip:name=mistream}",
      "options": []
    }
  ],
  "output": [
    {
      "id": "out",
      "address": "rtmp://...",
      "options": ["-c", "copy", "-f", "flv"]
    }
  ],
  "options": ["-loglevel", "level+info"]
}

El Core expande {whip:name=mistream}http://localhost:8555/whip/mistream/sdp e inyecta automáticamente -protocol_whitelist file,crypto,http,rtp,udp,tcp.

Si la UI no usa el placeholder {whip}, usar directamente:

{
  "address": "http://localhost:8555/whip/mistream/sdp",
  "options": ["-protocol_whitelist", "file,crypto,http,rtp,udp,tcp"]
}

3.7 GET /api/v3/process/:id/probe

Obtener información del stream (resolución, codec, fps) una vez el proceso existe.

Response 200 (cuando OBS está transmitiendo):

{
  "streams": [
    {
      "type": "video",
      "codec": "h264",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "pix_fmt": "yuv420p"
    },
    {
      "type": "audio",
      "codec": "opus",
      "sampling_hz": 48000,
      "layout": "stereo",
      "channels": 2
    }
  ]
}

Tabla resumen

Endpoint Método Cuándo usarlo
/api/v3/whip GET Polling streams activos (cada 5s)
/api/v3/whip/url GET Panel WHIP Server — info base del servidor
/api/v3/whip/:name/url GET Generar URL para OBS dado un stream key
/api/v3/config GET Leer estado enable/disable, puerto, token
/api/v3/config PATCH Cambiar configuración desde settings
/api/v3/process POST Crear proceso con WHIP como input
/api/v3/process/:id/probe GET Detectar resolución/codec del stream

Parte 4 — Instrucciones para el usuario en la UI

En el panel WHIP Server

Cómo transmitir desde OBS:
1. Abrí OBS → Configuración → Transmisión
2. Servicio: Personalizado
3. Servidor: http://<host>:<puerto>/whip/<tu-stream-key>
4. Clave de retransmisión: (dejar vacío)
5. Hacé clic en "Iniciar transmisión"

En el Receive mode WHIP

Ingresá tu stream key, copiá la URL y configurala en OBS.
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:

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.
// 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)

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.

  2. ICE / WebRTC: El Core maneja ICE + DTLS-SRTP internamente. La UI no necesita gestionar nada de WebRTC, solo mostrar la URL HTTP.

  3. Protocolo WHIP: El cliente (OBS) hace un POST /whip/<key> con SDP offer → el Core responde con SDP answer → ICE handshake → fluye RTP → el Core lo relay internamente a FFmpeg.

  4. protocol_whitelist: Es obligatorio cuando FFmpeg lee la URL /whip/<key>/sdp. El Core lo inyecta automáticamente cuando detecta la URL en el config del proceso, pero si la UI construye el comando FFmpeg directamente debe asegurarse de incluirlo.

  5. Token de autenticación: Si config.whip.token está configurado, la URL de publicación para OBS debe incluirlo como query param: ?token=<value>. El OBS lo enviará en cada request al WHIP endpoint.

  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

[
  {
    "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

{
  "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

{
  "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/<stream-key>",
  "input_address_template": "{whip:name=<stream-key>}"
}

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 <token>   (solo si has_token = true)

Response 201 Created:

Content-Type: application/sdp
Location: /whip/mystream/whep/<subid>
Body: <SDP answer con ICE candidates>

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/<subid>

Donde <subid> 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:

async function startWHEPPreview(streamKey, whepBaseUrl) {
  const pc = new RTCPeerConnection({ iceServers: [] })

  // Set up the MediaStream BEFORE creating the offer so that ontrack fires
  // correctly regardless of how fast ICE negotiation completes.
  // Using a MediaStream instead of e.streams[0] avoids the common bug where
  // e.streams is empty (no a=msid in SDP) and srcObject never gets set.
  const mediaStream = new MediaStream()
  videoElement.srcObject = mediaStream

  pc.ontrack = (e) => {
    mediaStream.addTrack(e.track)
  }

  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 <video>
  // NOTA: ontrack ya está registrado arriba, los tracks se agregan a mediaStream automáticamente.
  // El elemento <video> ya tiene srcObject = mediaStream desde el inicio.

  // Guardar para poder cerrar después
  return { pc, locationHeader }
}

async function stopWHEPPreview(pc, locationHeader, whipServerBase) {
  pc.close()
  if (locationHeader) {
    await fetch(whipServerBase + locationHeader, { method: 'DELETE' })
  }
}

Elemento HTML necesario:

<video id="whep-preview" autoplay playsinline muted controls></video>

Usar muted inicialmente para evitar bloqueo de autoplay en Chrome/Firefox.
Mostrar un botón de unmute para que el usuario active el audio voluntariamente.


6.4 CORS — el browser puede llamar directamente

El servidor WHIP responde con los headers CORS correctos en todos los endpoints:

Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Methods: POST, DELETE, OPTIONS
Access-Control-Expose-Headers: Location

No se necesita proxy en la UI. El fetch() al puerto 8555 funciona desde el browser sin problemas de CORS.


6.5 Indicadores de estado en el panel

Situación Qué mostrar
OBS publicando, subscribers: 0 Badge "En vivo" + botón "Preview"
OBS publicando, subscribers: N Badge "En vivo" + "N viewers" + botón "Preview activo"
OBS no publicando Badge "Sin señal" — ocultar botón Preview
WHEP no soportado (token WebRTC no disponible) No mostrar botón Preview

Actualizar el contador de subscribers con el mismo polling de 5s de GET /api/v3/whip.


6.6 Tabla resumen de todos los endpoints WHEP/WHIP activos

Endpoint Puerto Método Descripción
/api/v3/whip 8080 GET Lista publishers + contador subscribers
/api/v3/whip/url 8080 GET URL base del servidor + base_whep_url
/api/v3/whip/:name/url 8080 GET URLs completas: publish, sdp, whep
/whip/:name/whep 8555 POST Suscribirse vía WHEP (desde browser)
/whip/:name/whep/:subid 8555 DELETE Cerrar suscripción WHEP

Parte 7 — Multi-egress con {whip-rtsp:name=X} (relay RTSP interno)

7.1 Problema que resuelve

El placeholder {whip:name=X} (Parte 2) funciona bien para un solo proceso FFmpeg que consume el stream WHIP. Sin embargo, si se crean dos o más procesos que usan {whip:name=X} con el mismo stream key (por ejemplo, re-emitir a YouTube + Facebook + Twitch desde un solo OBS), solo uno puede leer el relay UDP loopback simultáneamente.

El relay RTSP interno resuelve esto: expone el stream como un servidor RTSP TCP al que pueden conectarse N clientes FFmpeg en paralelo, cada uno recibiendo una copia independiente.

OBS → WHIP → Core
               ├── {whip:name=X}      → proceso: HLS/memfs (1 consumidor)
               └── RTSP relay :8554
                       ├── {whip-rtsp:name=X} → proceso: YouTube
                       ├── {whip-rtsp:name=X} → proceso: Facebook
                       └── {whip-rtsp:name=X} → proceso: Twitch

7.2 Requisito en config

El relay RTSP debe estar habilitado en la configuración del Core:

"whip": {
  "enable": true,
  "address": ":8555",
  "token": "",
  "rtsp_address": ":8554"
}

Si rtsp_address está vacío, el relay no arranca y {whip-rtsp} no funciona.

7.3 Placeholder de proceso

En el input.address de cualquier proceso de egress:

{whip-rtsp:name=<stream_key>}

El Core lo expande a rtsp://127.0.0.1:8554/live/<stream_key> e inyecta automáticamente -rtsp_transport tcp en las input options (para forzar RTP/AVP/TCP interleaved, el único modo soportado por el relay interno).

Alternativa sin placeholder (si la UI no usa el mecanismo de templates):

{
  "address": "rtsp://127.0.0.1:8554/live/mistream",
  "options": ["-rtsp_transport", "tcp"]
}

En este caso la UI debe incluir -rtsp_transport tcp manualmente.

7.4 Diferencias con {whip}

{whip:name=X} {whip-rtsp:name=X}
Protocolo HTTP → SDP → UDP RTP RTSP/TCP (interleaved)
Consumidores simultáneos 1 N (ilimitado)
Requiere rtsp_address en config No
Auto-inyección de opciones FFmpeg -protocol_whitelist … -rtsp_transport tcp
Latencia adicional Ninguna Ninguna (~igual)
Uso típico Proceso principal de grabación/HLS Procesos de re-streaming a plataformas

7.5 UI en el wizard de proceso — modo "WHIP Multi-Egress"

Cuando el usuario selecciona WHIP como fuente de entrada en un proceso, mostrar un selector de modo:

┌─────────────────────────────────────────────────────────────┐
│  Modo de acceso al stream WHIP                              │
│                                                             │
│  ○ Directo  — Un solo proceso lee el stream (recomendado    │
│               para grabación/HLS mientras OBS transmite)    │
│                                                             │
│  ○ Relay RTSP — Múltiples procesos pueden leer              │
│                 simultáneamente (YouTube + Facebook + …)    │
│                 Requiere "Puerto relay RTSP" configurado     │
└─────────────────────────────────────────────────────────────┘

Modo Directoinput.address = "{whip:name=<stream_key>}"
Modo Relay RTSPinput.address = "{whip-rtsp:name=<stream_key>}"

Si el modo Relay RTSP está seleccionado y config.whip.rtsp_address está vacío, mostrar un aviso:

⚠️ El relay RTSP no está configurado. Activalo en Configuración → WHIP Server → Puerto relay RTSP.

7.6 Creación de proceso via API

Payload para proceso de re-streaming a YouTube usando relay RTSP:

{
  "id": "obs-to-youtube",
  "reference": "obs-to-youtube",
  "input": [
    {
      "id": "in",
      "address": "{whip-rtsp:name=mistream}",
      "options": []
    }
  ],
  "output": [
    {
      "id": "out",
      "address": "rtmp://a.rtmp.youtube.com/live2/STREAM_KEY",
      "options": ["-c", "copy", "-f", "flv"]
    }
  ],
  "options": ["-loglevel", "level+info"]
}

El Core expande {whip-rtsp:name=mistream}rtsp://127.0.0.1:8554/live/mistream e inyecta -rtsp_transport tcp automáticamente en las input options.

Tres procesos de egress simultáneos (YouTube + Facebook + Twitch):

// proceso 1
{ "input": [{ "address": "{whip-rtsp:name=mistream}" }], "output": [{ "address": "rtmp://youtube..." }] }

// proceso 2
{ "input": [{ "address": "{whip-rtsp:name=mistream}" }], "output": [{ "address": "rtmp://facebook..." }] }

// proceso 3
{ "input": [{ "address": "{whip-rtsp:name=mistream}" }], "output": [{ "address": "rtmp://twitch..." }] }

Los tres leen del relay RTSP independientemente; OBS solo transmite una vez.

7.7 Tabla resumen de endpoints actualizada

Endpoint Puerto Método Descripción
/api/v3/whip 8080 GET Lista publishers + subscribers
/api/v3/whip/url 8080 GET URL base + base_whep_url
/api/v3/whip/:name/url 8080 GET URLs completas: publish, sdp, whep
/whip/:name 8555 POST Publicar stream WHIP (OBS/clientes)
/whip/:name/sdp 8555 GET SDP para FFmpeg (modo directo)
/whip/:name/whep 8555 POST Suscribirse vía WHEP (browser)
/whip/:name/whep/:subid 8555 DELETE Cerrar suscripción WHEP
rtsp://localhost/live/:name 8554 Relay RTSP multi-consumer (TCP only)

Parte 8 — Panel "Publicaciones" (re-streaming a redes sociales)

8.1 Concepto general

El usuario transmite una sola vez desde OBS hacia el Core (WHIP). El Core re-emite el stream a todas las plataformas configuradas en paralelo (YouTube, Twitch, Facebook, TikTok, etc.). Cada destino es un proceso FFmpeg independiente que lee del relay RTSP interno.

OBS ──WHIP──► Core RTSP relay :8554
                      ├──► proceso "YouTube"   → rtmp://a.rtmp.youtube.com/live2/<key>
                      ├──► proceso "Twitch"    → rtmp://live.twitch.tv/app/<key>
                      ├──► proceso "Facebook"  → rtmps://live-api-s.facebook.com:443/rtmp/<key>
                      └──► proceso "TikTok"    → rtmp://...

Cada proceso se crea con POST /api/v3/process y puede iniciarse/detenerse individualmente.


8.2 ⚠️ Limitación crítica de audio — Opus → AAC obligatorio

OBS envía audio Opus (48 kHz, estéreo) via WHIP. El protocolo RTMP/FLV (que usan YouTube, Twitch, Facebook y TikTok) no soporta Opus; solo acepta AAC o MP3.

Por lo tanto, en todos los procesos de publicación a redes sociales:

  • Video: -c:v copy — H264 pasa sin recodificación
  • Audio: -c:a aac -b:a 128k — Opus se transcodifica a AAC en tiempo real
  • -c:a copy está PROHIBIDO para salidas RTMP — el stream llegaría corrupto o mudo

El FFmpeg interno del Core ejecuta el proceso equivalente a:

ffmpeg -rtsp_transport tcp \
       -i rtsp://127.0.0.1:8554/live/<stream_key> \
       -c:v copy \
       -c:a aac -b:a 128k \
       -f flv \
       rtmp://a.rtmp.youtube.com/live2/<youtube_key>

8.3 Payload de proceso por plataforma

YouTube

{
  "id": "pub-youtube",
  "reference": "pub-youtube",
  "input": [
    {
      "id": "in",
      "address": "{whip-rtsp:name=<stream_key>}",
      "options": []
    }
  ],
  "output": [
    {
      "id": "out",
      "address": "rtmp://a.rtmp.youtube.com/live2/<YOUTUBE_STREAM_KEY>",
      "options": ["-c:v", "copy", "-c:a", "aac", "-b:a", "128k", "-f", "flv"]
    }
  ],
  "options": ["-loglevel", "level+info"],
  "reconnect": true,
  "reconnect_delay_seconds": 5,
  "autostart": false
}

Twitch

{
  "id": "pub-twitch",
  "reference": "pub-twitch",
  "input": [
    {
      "id": "in",
      "address": "{whip-rtsp:name=<stream_key>}",
      "options": []
    }
  ],
  "output": [
    {
      "id": "out",
      "address": "rtmp://live.twitch.tv/app/<TWITCH_STREAM_KEY>",
      "options": ["-c:v", "copy", "-c:a", "aac", "-b:a", "128k", "-f", "flv"]
    }
  ],
  "options": ["-loglevel", "level+info"],
  "reconnect": true,
  "reconnect_delay_seconds": 5,
  "autostart": false
}

Facebook Live (RTMPS — requiere soporte TLS en FFmpeg)

{
  "id": "pub-facebook",
  "reference": "pub-facebook",
  "input": [
    {
      "id": "in",
      "address": "{whip-rtsp:name=<stream_key>}",
      "options": []
    }
  ],
  "output": [
    {
      "id": "out",
      "address": "rtmps://live-api-s.facebook.com:443/rtmp/<FACEBOOK_STREAM_KEY>",
      "options": ["-c:v", "copy", "-c:a", "aac", "-b:a", "128k", "-f", "flv"]
    }
  ],
  "options": ["-loglevel", "level+info"],
  "reconnect": true,
  "reconnect_delay_seconds": 5,
  "autostart": false
}

⚠️ Facebook e Instagram usan RTMPS (RTMP sobre TLS, puerto 443). Verificar que el binario FFmpeg del Core esté compilado con --enable-openssl o --enable-gnutls. Si no, el proceso fallará con Protocol not found.

TikTok Live

{
  "id": "pub-tiktok",
  "reference": "pub-tiktok",
  "input": [
    {
      "id": "in",
      "address": "{whip-rtsp:name=<stream_key>}",
      "options": []
    }
  ],
  "output": [
    {
      "id": "out",
      "address": "rtmp://<TIKTOK_SERVER_URL>/<TIKTOK_STREAM_KEY>",
      "options": ["-c:v", "copy", "-c:a", "aac", "-b:a", "128k", "-f", "flv"]
    }
  ],
  "options": ["-loglevel", "level+info"],
  "reconnect": true,
  "reconnect_delay_seconds": 5,
  "autostart": false
}

TikTok entrega la URL del servidor y la clave desde el Creator Center. La URL tiene formato rtmp://<servidor>/live/<clave>.

RTMP genérico (cualquier plataforma)

{
  "id": "pub-custom",
  "reference": "pub-custom",
  "input": [
    {
      "id": "in",
      "address": "{whip-rtsp:name=<stream_key>}",
      "options": []
    }
  ],
  "output": [
    {
      "id": "out",
      "address": "<RTMP_URL>",
      "options": ["-c:v", "copy", "-c:a", "aac", "-b:a", "128k", "-f", "flv"]
    }
  ],
  "options": ["-loglevel", "level+info"],
  "reconnect": true,
  "reconnect_delay_seconds": 5,
  "autostart": false
}

8.4 URLs RTMP de plataformas principales

Plataforma URL del servidor Protocolo
YouTube rtmp://a.rtmp.youtube.com/live2/<key> RTMP
YouTube (backup) rtmp://b.rtmp.youtube.com/live2/<key> RTMP
Twitch rtmp://live.twitch.tv/app/<key> RTMP
Twitch (ingest global) rtmp://ingest.global-contribute.live-video.net/app/<key> RTMP
Facebook rtmps://live-api-s.facebook.com:443/rtmp/<key> RTMPS
Instagram rtmps://edgetee-upload-<region>.facebook.com:443/rtmp/<key> RTMPS
TikTok URL dinámica desde Creator Center RTMP
Kick rtmp://ingest.kick.com/app/<key> RTMP
LinkedIn rtmps://stream.linkedin.com:443/live/<key> RTMPS
X (Twitter) rtmps://ingest.pscp.tv:443/x/<key> RTMPS

8.5 Panel "Publicaciones" en la UI

Estructura visual

┌─────────────────────────────────────────── Publicaciones ────┐
│  Stream activo: mistream  ● En vivo                          │
│  [+ Agregar destino]                                         │
│                                                              │
│ ┌─────────────┬──────────────────────────┬──────┬─────────┐  │
│ │ Plataforma  │ URL destino              │Estado│ Acciones│  │
│ ├─────────────┼──────────────────────────┼──────┼─────────┤  │
│ │ ▶ YouTube   │ rtmp://…/live2/xxxx      │ ✓ OK │ ■ Stop  │  │
│ │ ■ Twitch    │ rtmp://…/app/xxxx        │ — —  │ ▶ Start │  │
│ │ ▶ Facebook  │ rtmps://…/rtmp/xxxx      │ ✓ OK │ ■ Stop  │  │
│ └─────────────┴──────────────────────────┴──────┴─────────┘  │
│                                                              │
│  [▶ Iniciar todos]  [■ Detener todos]                        │
└──────────────────────────────────────────────────────────────┘

Estado de cada destino

Leer de GET /api/v3/process/<id>:

  • order: "start" + state: "running" → ✓ En vivo (verde)
  • order: "start" + state: "failed" → ✗ Error (rojo) + log disponible
  • order: "stop" → Detenido (gris)
  • proceso no existe → No configurado

8.6 Formulario "Agregar destino"

┌──────────────────────────────── Nuevo destino ────┐
│                                                    │
│  Plataforma:  [YouTube  ▼]                         │
│                (YouTube / Twitch / Facebook /      │
│                 TikTok / Kick / Personalizado)     │
│                                                    │
│  Stream Key:  [xxxx-xxxx-xxxx-xxxx        ]        │
│                                                    │
│  Servidor:    [rtmp://a.rtmp.youtube.com/live2/]   │
│               (autocompletado por plataforma,      │
│                editable para "Personalizado")      │
│                                                    │
│  Audio:       [AAC 128k ▼]                         │
│               (AAC 96k / AAC 128k / AAC 192k)      │
│               ⚠️ No usar "Copy" — OBS envía Opus   │
│                                                    │
│  Identificador del proceso: [pub-youtube  ]        │
│                                                    │
│  [Cancelar]                          [Guardar]     │
└────────────────────────────────────────────────────┘

Al guardar, la UI llama POST /api/v3/process con el payload de la sección 8.3 correspondiente.


8.7 Flujo completo de la UI

// Presets de plataformas
const PLATFORM_PRESETS = {
  youtube:  { name: 'YouTube',  server: 'rtmp://a.rtmp.youtube.com/live2/',       protocol: 'rtmp'  },
  twitch:   { name: 'Twitch',   server: 'rtmp://live.twitch.tv/app/',             protocol: 'rtmp'  },
  facebook: { name: 'Facebook', server: 'rtmps://live-api-s.facebook.com:443/rtmp/', protocol: 'rtmps' },
  tiktok:   { name: 'TikTok',   server: '',                                        protocol: 'rtmp'  },
  kick:     { name: 'Kick',     server: 'rtmp://ingest.kick.com/app/',             protocol: 'rtmp'  },
  custom:   { name: 'Custom',   server: '',                                        protocol: 'rtmp'  },
}

// Construir payload de proceso
function buildPublishProcess(processId, streamKey, platform, rtmpKey, audioBitrate = '128k') {
  const preset = PLATFORM_PRESETS[platform]
  const rtmpUrl = preset.server + rtmpKey

  return {
    id: processId,
    reference: processId,
    input: [{
      id: 'in',
      address: `{whip-rtsp:name=${streamKey}}`,
      options: [],
    }],
    output: [{
      id: 'out',
      address: rtmpUrl,
      options: ['-c:v', 'copy', '-c:a', 'aac', '-b:a', audioBitrate, '-f', 'flv'],
    }],
    options: ['-loglevel', 'level+info'],
    reconnect: true,
    reconnect_delay_seconds: 5,
    autostart: false,
  }
}

// Crear proceso
async function addDestination(processId, streamKey, platform, rtmpKey, audioBitrate) {
  const payload = buildPublishProcess(processId, streamKey, platform, rtmpKey, audioBitrate)
  await api.post('/api/v3/process', payload)
}

// Iniciar proceso
async function startDestination(processId) {
  await api.put(`/api/v3/process/${processId}/command`, { command: 'start' })
}

// Detener proceso
async function stopDestination(processId) {
  await api.put(`/api/v3/process/${processId}/command`, { command: 'stop' })
}

// Iniciar todos los destinos configurados
async function startAll(processIds) {
  await Promise.all(processIds.map(id => startDestination(id)))
}

// Detener todos
async function stopAll(processIds) {
  await Promise.all(processIds.map(id => stopDestination(id)))
}

// Leer estado de un proceso
async function getStatus(processId) {
  const { data } = await api.get(`/api/v3/process/${processId}`)
  return data  // { order: 'start'|'stop', state: 'running'|'failed'|'idle', ... }
}

8.8 Control de inicio/detención via API

Iniciar un proceso (equivale al botón ▶ Start):

PUT /api/v3/process/<id>/command
Body: { "command": "start" }

Detener un proceso (■ Stop):

PUT /api/v3/process/<id>/command
Body: { "command": "stop" }

Leer estado actual:

GET /api/v3/process/<id>
Response:
{
  "id": "pub-youtube",
  "order": "start",
  "state": "running",   // "running" | "idle" | "failed" | "starting" | "finishing"
  "config": { ... }
}

8.9 Mostrar logs de error

Si state === "failed", mostrar el log del proceso para diagnóstico:

GET /api/v3/process/<id>/log
Response: { "log": [ { "ts": 1234567890, "data": "..." }, ... ] }

Causas comunes de fallo:

Error en log Causa Solución
Connection refused / Connection timed out Stream key incorrecta o plataforma caída Verificar key en la plataforma
Codec not found: aac FFmpeg sin soporte AAC (raro) Imagen Docker correcta
Protocol not found: rtmps FFmpeg sin TLS Solo aplica a Facebook/Instagram — usar imagen con OpenSSL
Invalid data found when processing input OBS no está transmitiendo Arrancar OBS primero, luego iniciar procesos
Connection reset by peer La plataforma cerró el stream (clave duplicada en otra sesión) Cerrar otras sesiones activas

8.10 Prerrequisito: relay RTSP habilitado

El relay RTSP debe estar activo para que {whip-rtsp} funcione. Verificar antes de mostrar el panel de publicaciones:

async function checkRTSPRelay() {
  const { data } = await api.get('/api/v3/config')
  const rtspAddress = data.config.whip?.rtsp_address ?? ''
  return rtspAddress !== ''
}

Si el relay no está configurado, mostrar aviso:

⚠️ El relay RTSP no está activo.
Para publicar en múltiples plataformas simultáneamente, activá el relay RTSP:
Configuración → WHIP Server → Puerto relay RTSP (ej: 8554)

Si rtsp_address está vacío el usuario puede usar un proceso con {whip:name=X} para publicar a una sola plataforma (sin relay RTSP); en ese caso los output options son los mismos.