Hardata ECP (External Control Protocol)
Versión del documento: 1.0
Contrato público cubierto: HARDATA ECP 1.0
Estado de referencia: implementación disponible al 19 de agosto de 2026
Documento público y autocontenido para desarrolladores de aplicaciones externas de control HARDATA. Los nombres de campos, versiones y envelopes que aparecen aquí forman parte del contrato de interoperabilidad y deben respetarse exactamente.
Importante: HARDATA ECP controla HARDATA HDX 5 ó DINESAT 13 en tiempo real mediante WebSocket. Para consultar o administrar metadata, materiales y tráfico publicitario mediante HTTP use la especificación separada HARDATA API.
Este documento es autocontenido. Una persona o una IA debe poder desarrollar una App cliente funcional sin consultar el código fuente ni documentación adicional.
1. Objetivo y modelo mental
La App no se conecta directamente a HDX Radio. Se conecta por WebSocket al Gateway local. El Gateway mantiene una única conexión Named Pipe con HDX Radio, autentica cada sesión lógica, arbitra quién puede operar, correlaciona respuestas y distribuye estados.
App 1 ─┐
App 2 ─┼─ WebSocket JSON ─ HARDATA ECP Gateway ─ Named Pipe JSON ─ HDX Radio 4
App N ─┘ sesión y control conexión única
HARDATA ECP 1.0 es el único protocolo público. Todos los mensajes usan el objeto raíz ecp; la App no conoce ni interpreta el protocolo privado entre Gateway y HDX.
App -- HARDATA ECP 1.0 --> Gateway -- protocolo interno --> HDX Radio
El Gateway resuelve sesión, autorización, control y traducción de comandos, respuestas y eventos. Esta separación permite evolucionar HDX sin trasladar esa complejidad a las aplicaciones clientes.
HARDATA ECP 1.0 es la primera versión del contrato público. El contrato utiliza exclusivamente el envelope ecp y los nombres de campos definidos en este documento; no existen envelopes, aliases ni acciones públicas heredadas. El protocolo HDXRC 1.2 continúa existiendo sólo como detalle interno entre Gateway y HDX.
2. Dirección, transporte y límites
Valores predeterminados:
- WebSocket:
ws://127.0.0.1:5180/hdxrc. - Transporte: mensajes WebSocket de texto UTF-8.
- Un mensaje WebSocket equivale a un documento JSON completo.
- Tamaño máximo:
1.048.576bytes UTF-8 (1 MiB). - El Gateway admite fragmentación WebSocket y reensambla hasta
EndOfMessage. - No se usan saltos de línea como delimitadores.
- El orden de mensajes de una conexión WebSocket se conserva.
Para otra máquina se puede configurar una URL ws:// o wss://. La instalación predeterminada escucha solo en loopback y no aporta TLS. Una publicación en red debe incorporar WSS, firewall y autenticación de infraestructura.
Endpoints HTTP útiles:
GET /: información del servicio.GET /health:{ "status": "ok", "pipeConnected": true|false, "websocketClients": N }.GET /ready: HTTP200si HDX está conectado al Gateway;503si no lo está.
/ready sirve para diagnóstico, pero la App debe observar también los mensajes connection.changed y el campo hdxConnected de su sesión.
3. Requisitos del cliente WebSocket
La implementación cliente debe:
- aceptar solamente mensajes de texto;
- reensamblar frames fragmentados;
- validar UTF-8;
- rechazar o ignorar mensajes mayores a 1 MiB;
- serializar sus propias escrituras concurrentes;
- conservar un bucle de recepción activo durante toda la sesión;
- generar un
requestIdúnico y no vacío para cada mensaje enviado; - tolerar campos adicionales, eventos nuevos y tipos desconocidos;
- no registrar contraseñas ni mensajes de autenticación completos.
4. Secuencia de conexión obligatoria
El orden correcto es:
Conectar WebSocket
→ enviar ECP `session.hello`
← recibir ECP `session.state`
← opcionalmente session.setup
→ enviar ECP `command.execute/authenticate`
← recibir ECP `command.result/applied`
→ pedir snapshots y suscripciones
→ si se operará: control.request
← esperar control.granted
→ enviar acciones mutantes
→ el control permanece hasta transferencia, liberación o desconexión
No se debe enviar una acción HARDATA ECP antes de session.hello. El Gateway exige session.hello dentro de los primeros 10 segundos o cierra el socket con PolicyViolation.
5. Sesión, autorización y control
5.1 Envelope
{
"ecp": {
"version": "1.0",
"type": "session.hello",
"requestId": "gw-client-001"
},
"data": {}
}
Reglas para la App:
- enviar siempre
version = "1.0"; - usar un
requestIdúnico por operación ECP; - tratar
datacomo objeto; - correlacionar respuestas cuando repitan el
requestId; - aceptar eventos espontáneos con
requestId = null.
5.2 session.hello
Debe ser el primer mensaje de aplicación:
{
"ecp": {
"version": "1.0",
"type": "session.hello",
"requestId": "hello-001"
},
"data": {
"clientId": "app-pc-estudio-01",
"clientName": "Aplicación externa",
"applicationType": "operator-ui",
"applicationId": "<id-estable-de-aplicación>",
"applicationKey": "<secreto aleatorio persistente>",
"machineName": "ESTUDIO-01",
"requestedRole": "observer"
}
}
Campos:
| Campo | Tipo | Requisito | Semántica |
|---|---|---|---|
clientId |
string | recomendado | Identidad estable de la instancia. Si falta, el Gateway usa el GUID de sesión. |
clientName |
string | recomendado | Nombre legible. Si falta, usa clientId. |
applicationType |
string | recomendado | Tipo de App, por ejemplo operator-ui, automation u observer. |
applicationId |
string | para controlar | Identidad estable de la instalación. |
applicationKey |
string | para controlar | Secreto aleatorio persistente de al menos 256 bits. |
machineName |
string | recomendado | Equipo mostrado por HDX durante la aprobación. |
requestedRole |
string | sí | controller u observer. Cualquier valor distinto de controller se interpreta como observer. |
HARDATA ECP 1.0 utiliza exclusivamente applicationType. No existen aliases públicos para este campo.
Respuesta: ecp.type = "session.state".
5.3 Estado de sesión
Ejemplo de session.state o control.granted:
{
"ecp": {
"version": "1.0",
"type": "session.state",
"requestId": "hello-001"
},
"data": {
"sessionId": "55ef054f-1708-4787-ab7f-185c739be002",
"clientId": "app-pc-estudio-01",
"requestedRole": "observer",
"grantedRole": "observer",
"controlState": "authenticationRequired",
"applicationAuthorization": "pending",
"hdxConnected": true,
"remoteControlAvailable": true,
"remoteControlArmed": false,
"connectionEpoch": "d9fdb740-5818-436a-99e5-8745fbc5a31e",
"controller": null,
"authenticated": false
}
}
Valores de controlState:
authenticationRequired: la sesión debe autenticarse.active: la sesión posee el control.available: HDX está armado y la sesión puede pedir control.waitingForHdxArm: HDX todavía no habilita operación remota.hdxProtocolUnavailable: el Gateway está conectado, pero todavía no dispone de una sesión válida con HDX.
controller, cuando no es null, contiene:
{
"sessionId": "...",
"clientId": "otra-app",
"clientName": "Operador principal",
"applicationType": "operator-ui"
}
5.4 Pedir control
{
"ecp": {
"version": "1.0",
"type": "control.request",
"requestId": "control-001"
},
"data": {}
}
Respuesta exitosa: control.granted. Respuesta negativa: control.denied.
La sesión debe estar autenticada, aprobada por HDX y HDX debe informar remoteControlArmed = true. Solo existe un controlador a la vez. Una solicitud válida transfiere el control: la última solicitud autorizada gana.
5.5 Duración del control
No existe lease ni heartbeat de aplicación. El control permanece asociado a la sesión hasta que otra App aprobada lo solicita, la App lo libera, se pierde la conexión, HDX deja de estar armado o se revoca la autorización.
HDX mantiene todas las identidades conocidas, incluidas las rechazadas. Una identidad desconocida genera una sola consulta al operador; rejected y revoked se recuerdan y no vuelven a producir solicitudes. La lista persistente de aplicaciones puede ser administrada por el operador desde las propiedades de control remoto de HDX.
5.6 Liberar control
{
"ecp": {
"version": "1.0",
"type": "control.release",
"requestId": "release-001"
},
"data": {}
}
La sesión vuelve a observer y recibe session.state.
5.7 Eventos Gateway
ecp.type |
Destinatario | Contenido principal |
|---|---|---|
session.state |
sesión | Estado actualizado. |
control.granted |
sesión | Control concedido. |
control.denied |
sesión | reason y controlador activo. |
control.revoked |
sesión | reason. |
control.changed |
todas | controller, reason. |
session.authorizationChanged |
sesión | Estado persistente decidido por HDX. |
connection.changed |
todas | hdxConnected, connectionEpoch. |
error |
sesión | code y message. |
Razones de control.denied:
authenticationRequiredwaitingForHdxArmauthorizationPendingapplicationRejectedauthorizationRevokednotController
Razones de revocación/cambio observables:
replacedreleaseddisconnectedhdxDisconnectedhdxNotArmedcontrol.granted
6. Comandos, resultados y errores
6.1 Envelope de acción
{
"ecp": {
"type": "command.execute",
"version": "1.0",
"requestId": "9b228692-203a-41bf-8871-ad2810c7be7a"
},
"data": {
"windowId": "musicWindow",
"action": "getWindowSnapshot",
"params": {}
}
}
Requisitos:
ecp.type:command.execute.ecp.version: string exacto1.0.ecp.requestId: string no vacío y único entre requests pendientes.data.windowId: identificador de ventana.data.action: nombre de acción.data.params: objeto JSON, incluso cuando esté vacío.
No usar data.value, envelopes antiguos ni parámetros compactados en strings.
Ventanas reconocidas por las suscripciones del Gateway:
adsWindowmusicWindowauxWindow
Las acciones globales usan windowId = "global".
6.2 Resultado de acción
{
"ecp": {
"type": "command.result",
"version": "1.0",
"requestId": "9b228692-203a-41bf-8871-ad2810c7be7a",
"sequence": 1203,
"timestampMonotonicMs": 92350000
},
"data": {
"action": "play",
"status": "applied",
"windowId": "musicWindow",
"playlistRevision": 81
}
}
Estados posibles:
accepted: aceptado pero todavía no terminal; mantener el request pendiente.applied: aplicado; resultado terminal exitoso.rejected: rechazado; resultado terminal fallido.
No todas las respuestas contienen playlistRevision. La App debe tolerar su ausencia.
6.3 Error HARDATA ECP
{
"ecp": {
"type": "error",
"version": "1.0",
"requestId": "9b228692-203a-41bf-8871-ad2810c7be7a",
"sequence": 1204,
"timestampMonotonicMs": 92350001
},
"data": {
"windowId": "musicWindow",
"action": "setActualById",
"code": "invalidParameters",
"message": "setActualById requires playlistItemId"
}
}
Un error es terminal. Los campos públicos canónicos son data.code y data.message; una App no debe depender de un objeto error adicional. El Gateway finaliza la correlación con la primera respuesta terminal y suprime respuestas privadas duplicadas posteriores.
7. Autenticación HDX
Enviar después de recibir session.setup:
{
"ecp": {
"type": "command.execute",
"version": "1.0",
"requestId": "auth-001"
},
"data": {
"windowId": "global",
"action": "authenticate",
"params": {
"username": "<usuario>",
"password": "<contraseña>"
}
}
}
La autenticación solo es exitosa al recibir command.result con el mismo requestId y data.status = "applied". No inferir éxito por la conexión ni por session.setup.
Errores posibles:
invalidCredentialsauthLockedremoteControlDisabled
HDX cuenta intentos fallidos sobre la conexión Pipe compartida. Evitar reintentos automáticos agresivos. Solicitar nuevas credenciales al usuario después de un rechazo.
Cuando session.state.controlState vale authenticationRequired o llega connection.changed con hdxConnected = false:
- marcar la sesión como no autenticada;
- dejar de enviar acciones;
- descartar estado dependiente del
connectionEpochanterior; - esperar reconexión;
- autenticar nuevamente;
- volver a pedir snapshots, playlist y suscripciones;
- solicitar control nuevamente si corresponde.
8. Acciones y parámetros implementados
8.1 Acciones que no requieren control
El Gateway considera de solo lectura o gestión local:
| Acción | windowId |
params |
Resultado |
|---|---|---|---|
authenticate |
global |
username, password |
Validado por HDX. |
getWindowSnapshot |
ventana | {} |
HDX emite snapshot.window. |
getPlaylist |
ventana | {} |
HDX emite snapshot.playlist. |
setPlayerStateSubscription |
ventana | rateMs |
Consumida y agregada por Gateway. |
clearPlayerStateSubscription |
ventana | {} |
Consumida por Gateway. |
setPlayerStateSubscription y clearPlayerStateSubscription responden localmente con command.result/applied y no se reenvían a HDX. La identidad completa de la App se declara una sola vez en session.hello.
8.2 Reproducción y estado
Estas acciones requieren control:
| Acción | Parámetros tipados | Semántica |
|---|---|---|
play |
{} |
Inicia o continúa reproducción. |
pause |
{} |
Pausa reproducción. |
stop |
{} |
Detiene reproducción. |
fadeOut |
{} |
Solicita el fade de la reproducción actual. El comportamiento posterior depende del estado local de HDX. |
airCue |
{} |
Ejecuta la orden Air Cue de la ventana. |
setPositionMs |
{ "positionMs": 42850 } |
Posiciona el material actual en milisegundos. |
setActualById |
{ "playlistItemId": "456", "expectedPlaylistRevision": 81 } |
Establece actual por identidad de playlist. La revisión es opcional y el runtime actual no la valida. |
setNextById |
igual al anterior | Establece siguiente por identidad. |
selectForPlayById |
{ "playlistItemId": "456" } |
Selecciona el item para reproducción. |
setTalkOver |
{ "enabled": true } |
Estado idempotente de talk-over. |
setStopAtEnd |
{ "enabled": true } |
Estado idempotente de detener al final. |
mainVolume |
{ "volume": 70 } |
Define el volumen principal. |
auxVolume |
{ "volume": 70 } |
Define volumen auxiliar. |
clearList |
{} |
Vacía la playlist de la ventana. |
toggleAscendingTimer |
{ "enabled": true } |
Configura timer ascendente. |
En setTalkOver y setStopAtEnd, enabled es obligatorio y booleano. Son asignaciones absolutas, no toggles: repetir enabled: true conserva la función activada y repetir enabled: false la conserva desactivada. El Gateway rechaza valores ausentes o de otro tipo con invalidParams; los reintentos y mensajes duplicados no deben invertir el estado en HDX.
Usar playlistItemId como string opaco aunque algunos valores parezcan números. No usar index como identidad persistente.
8.3 Edición de playlist
Todas requieren control:
| Acción | Parámetros |
|---|---|
selectItems |
{ "playlistItemIds": "101,102,103" } |
deleteSelected |
{ "playlistItemIds": "101,102,103" } |
copySelected |
{ "playlistItemIds": "101,102,103" } |
pasteClipboard |
{ "anchorPlaylistItemId": "105" }; el ancla es opcional. |
moveItemBefore |
{ "playlistItemId": "103", "beforePlaylistItemId": "101" }; destino opcional según operación. |
insertMaterial |
{ "anchorPlaylistItemId": "105", "materialId": "9001" } |
insertOverlay |
{ "anchorPlaylistItemId": "105", "materialId": "9002", "offsetSeconds": 3 }; offset opcional. |
insertBlock |
{ "title": "Bloque", "time": "12:30:00" } |
La implementación actual transporta playlistItemIds como una cadena separada por comas, no como array JSON.
8.4 Extensibilidad
El contrato público comprende únicamente las acciones documentadas en esta sección. Una App no debe inventar nombres de acción ni depender de comportamientos internos no documentados. Las extensiones futuras se documentarán y versionarán dentro de HARDATA ECP.
9. Suscripción a playerStatus
Alta:
{
"ecp": {
"type": "command.execute",
"version": "1.0",
"requestId": "sub-music-001"
},
"data": {
"windowId": "musicWindow",
"action": "setPlayerStateSubscription",
"params": { "rateMs": 500 }
}
}
Baja: misma estructura con action = "clearPlayerStateSubscription" y params = {}.
Reglas:
- El Gateway aplica por App un clamp de
100a10000 ms. - Si falta
rateMs, usa500 ms. - Solo agrega suscripciones de Apps autenticadas.
- Hacia HDX solicita la cadencia más rápida requerida por todas las Apps.
- Cada App recibe
playerStatuscomo máximo según su propia cadencia. - Sin suscripción para una ventana, la App no recibe
playerStatusde esa ventana. - Los demás eventos públicos no dependen de esta suscripción.
- HDX puede emitir más rápido en transiciones; el Gateway sigue aplicando el límite individual de la App.
10. Eventos y snapshots HARDATA ECP
10.1 Envelope de estado
{
"ecp": {
"version": "1.0",
"type": "state.playbackStateChanged",
"sequence": 991204,
"timestampMonotonicMs": 92345678
},
"data": {}
}
sequence es monotónica por ventana para los estados emitidos por HDX. timestampMonotonicMs es tiempo monotónico del proceso HDX y no es una fecha UTC. La App debe guardar lastSequence por ventana y descartar un estado si sequence <= lastSequence.
El evento global remoteControlStateChanged actualmente incluye sequence pero puede no incluir timestampMonotonicMs; tolerar su ausencia.
10.2 remoteControlStateChanged
{
"ecp": {
"version": "1.0",
"type": "state.remoteControlStateChanged",
"sequence": 20
},
"data": {
"remoteControlAvailable": true,
"remoteControlArmed": true
}
}
remoteControlArmed es la condición efectiva para que el Gateway conceda y conserve el control. Los modos automáticos pertenecen exclusivamente a la operación local de HDX: ECP no los expone ni permite modificarlos. Las acciones internas autoMode y setAutoEnabled reciben actionNotAvailable si una App intenta enviarlas.
EXT CTRL es únicamente la etiqueta actual del botón de HDX que antes se llamaba RC. No es un modo, estado, comando ni valor de HARDATA ECP. Para que HDX informe remoteControlArmed = true, el operador debe presionar ese botón y mantener activo AUTO; ambas operaciones se realizan localmente en HDX.
10.3 playbackStateChanged
{
"ecp": {
"version": "1.0",
"type": "state.playbackStateChanged",
"sequence": 101,
"timestampMonotonicMs": 45000
},
"data": {
"windowId": "musicWindow",
"playbackState": "playing",
"talkOverEnabled": false,
"stopAtEndEnabled": false,
"mainVolume": 100,
"auxVolume": 70
}
}
Estados de reproducción esperados: stopped, playing, paused. Tolerar valores futuros.
10.4 actualNextChanged
{
"ecp": {
"version": "1.0",
"type": "state.actualNextChanged",
"sequence": 102,
"timestampMonotonicMs": 45100
},
"data": {
"windowId": "musicWindow",
"actualPlaylistItemId": "456",
"nextPlaylistItemId": "489"
}
}
Ambos identificadores pueden ser null.
10.5 playerStatus
{
"ecp": {
"version": "1.0",
"type": "state.playerStatus",
"sequence": 103,
"timestampMonotonicMs": 45200
},
"data": {
"windowId": "musicWindow",
"onAirTimer": "00:12:34",
"itemTimer": "00:01:28",
"playlistItemId": "456",
"materialId": "123",
"positionMs": 42850,
"durationMs": 215000,
"remainingMs": 172150,
"phase": "intro",
"playbackState": "playing",
"actualPlaylistItemId": "456",
"nextPlaylistItemId": "489",
"vuLeft": 73,
"vuRight": 69
}
}
Los cuatro identificadores pueden ser strings o null. positionMs, durationMs y remainingMs son la referencia temporal canónica. Los timers formateados son para presentación. phase puede cambiar con la evolución del reproductor; no usar una enumeración cerrada.
10.6 snapshot.window
Respuesta a getWindowSnapshot:
{
"ecp": {
"version": "1.0",
"type": "snapshot.window",
"sequence": 104,
"timestampMonotonicMs": 45300
},
"data": {
"windowId": "musicWindow",
"playbackState": "playing",
"talkOverEnabled": false,
"stopAtEndEnabled": false,
"mainVolume": 100,
"auxVolume": 70,
"actualPlaylistItemId": "456",
"nextPlaylistItemId": "489",
"positionMs": 42850,
"durationMs": 215000,
"remainingMs": 172150,
"playbackPhase": "intro",
"playlistRevision": 81
}
}
El snapshot no incrementa playlistRevision.
10.7 snapshot.playlist y playlist.updated
Los nombres snapshot.playlist y playlist.updated aparecen directamente en ecp.type:
{
"ecp": {
"type": "snapshot.playlist",
"version": "1.0",
"sequence": 105,
"timestampMonotonicMs": 45400
},
"data": {
"schemaVersion": "1.2",
"windowId": "musicWindow",
"playlistRevision": 81,
"actualIndex": 4,
"nextIndex": 5,
"nodes": [
{
"playlistItemId": "456",
"materialId": "123",
"index": 4,
"nodeType": "material",
"parentIndex": null,
"level": 1,
"childrenCount": 0,
"relativeStartOffsetSeconds": null,
"title": "Título",
"playStatus": "actual",
"dcs": "",
"mediaType": "audio",
"overType": ""
}
]
}
}
Reglas:
snapshot.playlistinforma la lista actual sin incrementar la revisión.playlist.updatedrepresenta una publicación posterior a mutación estructural e incrementa la revisión.actualIndex,nextIndex,parentIndex,relativeStartOffsetSeconds,playlistItemIdymaterialIdpueden sernull.playlistItemIdidentifica una instancia concreta dentro de la playlist; se utiliza para seleccionar, mover u operar sobre ese nodo mediante HARDATA ECP.materialIdidentifica el material en la plataforma HARDATA; se utiliza para consultar su metadata y recursos relacionados mediante HARDATA API.playlistItemIdymaterialIdno son intercambiables: un mismo material puede aparecer varias veces en una playlist y cada aparición tendrá su propioplaylistItemId.- Tratar ambos identificadores como strings opacos, aunque su contenido parezca numérico. No deducir significado ni construirlos desde otros campos.
materialIdpuede sernullen bloques, agrupadores u otros nodos que no representen un material consultable mediante HARDATA API.indexes solo la posición actual del nodo y no debe utilizarse como identidad persistente.- Una recarga completa puede reasignar identificadores.
- Reemplazar localmente la playlist completa por
data.nodesal recibir cualquiera de los dos tipos. - Actualizar de forma atómica
playlistRevision, punteros e items.
11. session.setup
HDX lo emite al completar el handshake interno. El Gateway guarda el último y lo entrega a cada sesión después de session.hello, incluso antes de autenticar:
{
"ecp": {
"type": "session.setup",
"version": "1.0"
},
"data": {
"remoteControlEnabled": true,
"authRequired": true,
"station": "Radio Uno",
"studio": "Aire 1",
"workstation": "ESTUDIO-A",
"workstationIP": "192.168.1.50",
"webRTC": "https://localhost:5100/webrtc",
"protocolVersion": "1.2",
"capabilities": [
"playlistItemId",
"correlatedCommands",
"atomicWindowSnapshot",
"adaptiveTimer"
]
}
}
La App debe tratar los campos como informativos y tolerar campos ausentes o capacidades nuevas. data.protocolVersion y los campos schemaVersion de otros mensajes describen datos internos transportados por el Gateway; no son otra versión del protocolo público ni deben usarse para despachar mensajes. Para ello se valida exclusivamente ecp.version. La recepción de session.setup no significa que la sesión esté autenticada.
12. Distribución, caché y correlación
El Gateway aplica estas reglas:
session.setup: para sesiones que enviaronsession.hello.- Resultados y errores con
requestId: solo para la sesión que originó el request. - Estados, snapshots y playlists públicos: solo para sesiones autenticadas.
playerStatus: además requiere suscripción para la ventana.
Antes de reenviar una acción a HDX, el Gateway sustituye el requestId por uno interno y agrega auditoría. Al volver la respuesta restaura el requestId original. Este mecanismo es transparente para la App.
El Gateway cachea el último mensaje público por connectionEpoch, ventana y evento/tipo. Al autenticar una App reproduce el cache ordenado por sequence. Por eso pueden llegar estados inmediatamente después del éxito de autenticación, antes de las respuestas a los snapshots solicitados por la App.
La App debe diseñar su store como un reductor idempotente, no asumir una secuencia rígida de eventos después de autenticar.
13. Errores generados por el Gateway
Todos los errores públicos usan ecp.type = "error":
{
"ecp": {
"version": "1.0",
"type": "error",
"requestId": "req-001"
},
"data": {
"code": "controlRequired",
"message": "Session does not own control"
}
}
| Código | Significado | Recuperación |
|---|---|---|
invalidJson |
JSON inválido. | Corregir serialización. |
invalidEnvelope |
Falta el objeto ecp. |
Corregir envelope. |
unsupportedVersion |
ecp.version no está soportada. |
Usar una versión publicada y aceptada por el Gateway. |
helloRequired |
Se operó antes de session.hello. |
Enviar session.hello. |
invalidParams |
Faltan parámetros públicos o tienen un tipo incorrecto. | Corregir data.params. |
authenticationRequired |
La sesión no está autenticada. | Ejecutar authenticate. |
controlRequired |
Acción mutante sin control. | Pedir control y esperar control.granted. |
authorizationPending |
HDX todavía no decidió. | Esperar session.authorizationChanged. |
applicationRejected |
HDX rechazó la identidad. | El operador debe cambiar la decisión en HDX. |
remoteControlNotArmed |
HDX no está armado para control externo. | En HDX, presionar EXT CTRL (antes RC) y activar AUTO; luego esperar remoteControlArmed = true. |
actionNotAvailable |
La acción pertenece a operación local de HDX y no forma parte de ECP. | No enviarla desde una App externa. |
hdxUnavailable |
Pipe desconectado o fallo de envío. | Esperar reconexión y reautenticar. |
requestTimeout |
HDX no terminó en el timeout configurado. | Marcar resultado desconocido y resincronizar; no repetir acciones no idempotentes automáticamente. |
unknownMessageType |
ecp.type desconocido. |
Corregir tipo. |
Errores HDX observables adicionales:
remoteControlDisabledauthRequiredinvalidCredentialsauthLockedinvalidWindowwindowNotOpeninvalidParametersnotAllowedinvalidStateinvalidTargetinvalidPlaylistItemIdinvalidItemId
La lista puede crecer. Mostrar data.message al operador y conservar data.code para lógica programática.
14. Máquina de estados recomendada
DISCONNECTED
└─ WebSocket abierto → HELLO_SENT
└─ session.state → UNAUTHENTICATED
└─ authenticate/applied → OBSERVING
├─ authorization approved → AUTHORIZED
│ └─ control.request/control.granted → CONTROLLING
│ └─ replaced/released/not armed → OBSERVING
└─ desconexión HDX → UNAUTHENTICATED
Cualquier cierre WebSocket → DISCONNECTED
No habilitar controles mutantes en UI salvo en CONTROLLING. Las consultas y suscripciones se habilitan en OBSERVING y CONTROLLING.
15. Estado local recomendado
Mantener como mínimo:
GatewayState
websocketConnected
hdxConnected
connectionEpoch
authenticated
requestedRole
grantedRole
ownsControl
applicationAuthorization
remoteControlAvailable
remoteControlArmed
controller
WindowState por windowId
lastSequence
playbackState
talkOverEnabled
stopAtEndEnabled
mainVolume
auxVolume
actualPlaylistItemId
nextPlaylistItemId
positionMs
durationMs
remainingMs
phase
vuLeft
vuRight
playlistRevision
playlist[]
PendingRequest por requestId
action
windowId
sentAt
idempotent
Al cambiar connectionEpoch, reiniciar lastSequence, pendientes, autenticación, control y estados cuya vigencia dependa de la conexión HDX anterior.
16. Algoritmo mínimo de implementación
Pseudocódigo:
connect(url):
abrir WebSocket
iniciar receiveLoop
send("session.hello", identity)
onMessage(json):
if json.ecp.version != "1.0":
log and ignore
return
switch json.ecp.type:
case "session.state", "session.authorizationChanged": update session
case "connection.changed": reset state if connectionEpoch changed
case "control.granted": ownsControl = true
case "control.denied", "control.revoked": ownsControl = false
case "command.result": correlate; finish unless status == "accepted"
case "error": correlate and finish as failure
case "snapshot.window": replace window snapshot
case "snapshot.playlist", "playlist.updated": replace playlist atomically
case type starts with "state.": apply event if sequence is newer
default: log and ignore
sendAction(window, action, params):
require authenticated
if action is mutating: require ownsControl
requestId = new UUID
store pending request
send text JSON with HARDATA ECP 1.0 envelope
17. Arranque recomendado después de autenticar
Para cada ventana que la App mostrará:
getWindowSnapshot.getPlaylist.setPlayerStateSubscriptionconrateMs = 500.
Para una App controladora:
- indicar al operador que, en HDX, presione
EXT CTRLy activeAUTOsi aún no están activos; - esperar
remoteControlArmed = true; - enviar
control.request; - esperar
control.granted; - comprobar que HDX informó autorización
approved; - habilitar controles mutantes.
No es necesario esperar secuencialmente los tres primeros resultados para procesar eventos. Correlacionar cada request de forma independiente.
18. Reconexión segura
Ante cierre WebSocket:
- marcar control y autenticación como perdidos;
- marcar requests pendientes como resultado desconocido;
- reconectar con backoff, por ejemplo 1, 2, 5, 10 segundos con jitter;
- repetir desde
session.hello.
Ante requestTimeout, no repetir automáticamente acciones mutantes como insertMaterial, deleteSelected, moveItemBefore o cambios de reproducción. La acción pudo haberse aplicado aunque su respuesta se haya perdido. Primero pedir snapshot.window y snapshot.playlist para reconciliar.
Las acciones idempotentes (setTalkOver, setStopAtEnd y valores absolutos de volumen/posición) pueden reintentarse solo después de resincronizar y recuperar el control.
19. Seguridad
- No guardar ni registrar contraseñas en texto plano.
- Enviar credenciales solo sobre loopback o WSS confiable.
- No exponer el token interno Gateway-HDX; una App nunca debe usar
gatewayHello. - Validar el certificado al usar WSS.
- Limitar tamaño antes de serializar/enviar.
- Escapar contenido visible como títulos; no interpretar HTML recibido.
- Tratar nombres, títulos y mensajes de error como datos no confiables.
- Separar permisos de observación de permisos de control en la UI.
20. Checklist de aceptación de una App
Transporte
- Conecta por
ws/wssy usa texto UTF-8. - Reensambla fragmentos y aplica límite de 1 MiB.
- Serializa envíos concurrentes.
Sesión
- Envía
session.helloinmediatamente. - Procesa todos los tipos HARDATA ECP 1.0 conocidos.
- Autentica cada nueva conexión/epoch.
- No confunde
session.setupcon autenticación exitosa.
Control
- Espera
control.grantedantes de mutar. - Espera autorización
approvedantes de solicitar control. - Deshabilita operación ante revocación, desarme o desconexión.
- Libera control al cerrar ordenadamente.
HARDATA ECP
- Usa
ecp.version = "1.0"yparamstipados. - Genera
requestIdúnico. - Correlaciona
command.result,errory errores Gateway. - Mantiene
acceptedpendiente hasta resultado terminal. - Usa
playlistItemIdcomo identidad y noindex. - Usa
materialId—cuando está presente— para consultar el material mediante HARDATA API. - No confunde
materialIdconplaylistItemId. - Descarta secuencias antiguas por ventana.
- Reemplaza playlist atómicamente.
- Tolera nulos, campos ausentes y extensiones.
Recuperación
- Reinicia estado al cambiar
connectionEpoch. - Reconstruye snapshots y suscripciones tras reconectar.
- No repite mutaciones no idempotentes después de timeout sin reconciliar.
21. Flujo completo de ejemplo
21.1 Identificación
{"ecp":{"version":"1.0","type":"session.hello","requestId":"g1"},"data":{"clientId":"demo-1","clientName":"Demo","applicationType":"operator-ui","requestedRole":"controller"}}
21.2 Autenticación
{"ecp":{"type":"command.execute","version":"1.0","requestId":"a1"},"data":{"windowId":"global","action":"authenticate","params":{"username":"<usuario>","password":"<contraseña>"}}}
Esperar:
{"ecp":{"type":"command.result","version":"1.0","requestId":"a1"},"data":{"action":"authenticate","status":"applied","windowId":"global"}}
21.3 Sincronización
{"ecp":{"type":"command.execute","version":"1.0","requestId":"s1"},"data":{"windowId":"musicWindow","action":"getWindowSnapshot","params":{}}}
{"ecp":{"type":"command.execute","version":"1.0","requestId":"s2"},"data":{"windowId":"musicWindow","action":"getPlaylist","params":{}}}
{"ecp":{"type":"command.execute","version":"1.0","requestId":"s3"},"data":{"windowId":"musicWindow","action":"setPlayerStateSubscription","params":{"rateMs":500}}}
21.4 Control
{"ecp":{"version":"1.0","type":"control.request","requestId":"g2"},"data":{}}
Solo después de control.granted:
{"ecp":{"type":"command.execute","version":"1.0","requestId":"p1"},"data":{"windowId":"musicWindow","action":"play","params":{}}}
21.5 Transferencia
Otra App aprobada puede enviar control.request. El controlador anterior recibe control.revoked con motivo replaced; no existe heartbeat de aplicación.
Con este contrato una App puede conectarse, autenticarse, observar estado, mantener playlists, adquirir control exclusivo, ejecutar acciones y recuperarse correctamente de desconexiones sin conocimiento del código interno del Gateway o de HDX Radio.