Protocolossession-ipc (control Bun ↔ daemon)messages

Media engine: cmd.media.openPackaging · cmd.media.closePackaging · qry.media.probe

Spec canónica de protocols/session-ipc/messages/media-engine.md, copiada sin reescribir.

ImplementadoSin versión del tren todavía· generada desde protocols/session-ipc/messages/media-engine.md

Página generada desde protocols/session-ipc/messages/media-engine.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere.

Media engine: cmd.media.openPackaging · cmd.media.closePackaging · qry.media.probe

Control del motor de medios dual (dec-0110 §3): Bun decide (qué motor por sesión, qué pistas, longitud de segmento), el daemon valida y ejecuta. Los bytes empaquetados (playlists HLS, init CMAF, fragmentos moof+mdat) no viajan por este socket: la respuesta de openPackaging trae las URLs HTTP/3 que el reproductor pide directo al daemon (r01).

Sobre spire como el resto del protocolo (v2, ../SESSION_PROTOCOL.md); contratos en packages/api-contracts/src/bus/media-daemon.ts (MediaOpenPackaging, MediaClosePackaging, MediaProbe). Implementación: native/zig/media-daemon/ipc/media_engine_ipc.zig y probe_ipc.zig. MediaEngines de v1 salió en v2 (sin consumidor); MediaProbe vuelve como qry.media.probe con consumidor real (el escaneo de catalog-svc, ME1).

OpenPackaging

{
  "rootId": "films",
  "relPath": "film.mkv",
  "engine": "auto",
  "segmentMs": 6000,
  "video": 0,
  "audio": 1,
  "includeAudio": true,
  "capActor": "<32 hex>",
  "capResource": "<64 hex>"
}
CampoTipoReq.Descripción
rootIdstringsíRaíz de STYX_MEDIA_ROOTS ([A-Za-z0-9._-]{1,64}); contrato versión 2 (dec-0117 I7)
relPathstringsíRuta relativa a la raíz, 1..4095 bytes; se abre fd-relativa (sin ../absoluta/symlinks)
enginestringnozig | libav | auto; por defecto STYX_MEDIA_ENGINE (otro valor: invalid_request)
segmentMsintnoDuración objetivo de segmento, 1000..20000 (defecto 6000); corta en keyframes
videointnoÍndice de pista de vídeo; defecto = la primera. Si no existe: error, no sustitución
audiointnoÍndice de pista de audio; defecto = la primera. Si no existe: error
includeAudioboolnoDefecto true
capActorstringnoDigest del actor (32 hex) al que se liga la sesión (dec-0117 I2); con capResource
capResourcestringnoDigest del recurso (64 hex, el asset); los dos o ninguno

Con capActor/capResource el daemon liga el pkg_* en su autoridad de capabilities antes de responder, igual que CreateSession liga el sess_*: /styx/pkg/<id>/... sólo sirve una petición con el SCT de lectura que playback-svc firma para ese pkg_*. Uno solo o malformado → CAPABILITY_BINDING_INVALID; sin autoridad o con el enlace imposible → CAPABILITY_BIND_FAILED (la sesión no queda abierta). Sin ninguno de los dos la sesión existe pero ninguna petición puede leerla (deny-by-default).

El daemon valida el motor pedido contra sus capacidades declaradas (contenedor + codec de cada pista a empaquetar); nunca lo asume. auto = Zig si Zig declara la capacidad, si no libav (si está compilado y capaz). Con STYX_MEDIA_ENGINE_FALLBACK=libav, un fallo de Zig por la entrada (malformed, truncated, unsupported…) reabre con libav y lo dice en fallback.

Respuesta:

{
  "result": {
    "packagingId": "pkg_0f3c…(32 hex)",
    "engine": "zig",
    "segments": 1204,
    "targetDurationS": 7,
    "codecs": "hvc1.2.4.L120.90,ec-3",
    "masterUrl": "https://media.example:4435/styx/pkg/pkg_0f3c…/master.m3u8",
    "mediaUrl": "https://media.example:4435/styx/pkg/pkg_0f3c…/index.m3u8",
    "tcpMasterUrl": "https://media.example:443/styx/pkg/pkg_0f3c…/master.m3u8",
    "certHash": "<64 hex>",
    "transportType": "h3"
  }
}

fallback (motivo del fallback de motor, ^[a-z_]{1,48}$) sólo aparece si lo hubo. Sin listener HTTP/3 no hay masterUrl/mediaUrl/certHash y transportType es "none".

tcpMasterUrl (contrato cmd.media.openPackaging v3, dec-0134) es la misma master playlist por el origen TCP del daemon (HTTPS HTTP/1.1 sobre TLS 1.3 con Alt-Svc hacia h3), en el host y el puerto que los clientes marcan (STYX_MEDIA_TCP_PUBLIC_PORT, por defecto el puerto TCP enlazado, que por defecto es el UDP de HTTP/3). Sólo aparece con origen TCP; playback-svc lo anuncia como delivery.tcpEndpoint del descriptor. masterUrl sigue siendo el recurso H3: un navegador que no conoce el origen por Alt-Svc usa tcpEndpoint.

Recursos servidos (HTTP/3 y el origen TCP, GET/HEAD)

Las mismas rutas, con la misma credencial, los mismos límites y los mismos códigos se sirven por HTTP/3 (UDP) y por el origen TCP del daemon (dec-0134): es el mismo despachador, no una copia. Las respuestas TCP llevan Alt-Svc: h3=":<puerto UDP>"; ma=86400 salvo STYX_MEDIA_TCP_ALT_SVC=off. Por TCP una conexión HTTP/1.1 atiende una petición a la vez (un Range o un segmento ocupa su conexión mientras dura), sin cuerpo de petición, Upgrade ni Transfer-Encoding; las cabeceras Authorization, Range, Origin y X-Styx-Request-Id no pueden repetirse (400).

RutaContent-Type
/styx/pkg/<packagingId>/master.m3u8application/vnd.apple.mpegurl
/styx/pkg/<packagingId>/index.m3u8application/vnd.apple.mpegurl
/styx/pkg/<packagingId>/init.mp4video/mp4
/styx/pkg/<packagingId>/seg-<n>.m4svideo/iso.segment

Credencial (dec-0117 I1): cada petición GET/HEAD lleva Authorization: Bearer <sct> (el SCT de lectura del pkg_*; cap= no se acepta aquí, las rutas de empaquetado no llevan query). Se comprueba contra el pkg_* de la ruta antes de tocar el store: sin credencial, con la de otra sesión, de otro scope, caducada o revocada (ClosePackaging), 403 genérico, exista la sesión o no (sin oráculo 404/403). El preflight OPTIONS no lleva credencial.

403 = sin capability válida para ese pkg_* · 404 = sesión o segmento desconocido (bajo un grant válido) · 400 = nombre malformado · 503 = segmento cancelado, por encima de un límite de admisión o sin productor a tiempo (abajo) · 500 = no se pudo producir.

Admisión de un segmento (GET .../seg-<n>.m4s), nada se encola sin pasarla, en este orden: primero los topes, 503 por encima de 4 segmentos en vuelo por conexión (en cola, produciéndose o enviándose), 4 en vuelo por sesión de empaquetado sumando todas sus conexiones (too many segment requests in flight for this packaging session), 256 en total o 32 esperando productor; después la sesión debe existir y tener ese segmento (Provider.track: 404/400 si no, con o sin x-styx-request-id). Una petición por encima de un tope recibe 503 aunque el segmento no exista. Una sesión produce un segmento cada vez, así que los demás suyos esperan en cola: el tope por sesión (por debajo de los 32 de la cola, y el daemon no arranca si no lo está) hace que un pkg_* —un SCT— abriendo tantas conexiones como quiera ocupe como mucho 4 puestos y deje sitio en la cola a los segmentos de otra sesión mientras haya un productor libre. Varias sesiones (varios SCT) sí pueden llenarla entre todas: el tope por usuario va con los presupuestos del threat model. Todo segmento admitido queda registrado en la sesión: ClosePackaging cancela los suyos (en cola: 503 Cancelled, nunca se producen; produciéndose: 503 Cancelled; enviándose: reset del stream). Uno pedido con x-styx-request-id: <id> es además cancelable con cmd.media.cancel{requestId: <id>, packagingId?: <pkg>} por el socket (r20 §3.6: por request, no global), que alcanza todos los segmentos en vuelo con ese id, sólo los de packagingId si viene. Un id repetido (en la misma sesión o en otra) no deja a nadie sin poder cancelarse. La sesión sigue sirviendo los demás.

Producción y envío. Los segmentos se producen fuera del hilo del event loop HTTP/3 (hilos productores, STYX_MEDIA_PACKAGING_WORKERS) y salen por ventanas: el stream nunca guarda más de 256 KiB sin enviar y el resto espera a onWritable. Bytes retenidos: un segmento que se produce cuenta lo más que puede ocupar (max_request_bytes más sus cajas, ~264 MiB) y uno producido su tamaño hasta que sale. STYX_MEDIA_PACKAGING_HELD_MIB (768 por defecto) es el tope global; la mitad es el tope de una conexión y el de una sesión de empaquetado, y el daemon no arranca si el tope global no cabe esa mitad más un segmento (mínimo 529). Un productor sólo toma un segmento cuya reserva cabe en los tres topes; si no, espera en cola (nada se produce para descartarse), así que lo que retenga una conexión o una sesión nunca impide producir el segmento de otra. Un productor tampoco toma un segundo segmento de una sesión que ya produce uno, ni más de uno a la vez de la misma conexión. Tiempo: un segmento que espera productor más de 10 s responde 503 segment queue timeout; uno que se está enviando debe salir, pasados 5 s, a una media de al menos 128 KiB/s desde que empezó, y terminar en 120 s como mucho; si no, se resetea y libera lo que retenía. Lo que un cuerpo retiene queda así acotado en bytes por los topes y en tiempo por los 120 s, lea como lea el cliente. Si la petición se cancela o la conexión se cierra, su segmento se cancela aunque se esté produciendo y no se responde nada. HEAD de un segmento responde 200 sin content-length y no lo produce (su tamaño sólo se sabe produciéndolo); HEAD de playlists e init lleva content-length.

Lectura cross-origin (reproductor web): STYX_MEDIA_ALLOWED_ORIGINS = lista de orígenes exactos (scheme://host[:port], separados por comas, máx. 16; sin * ni null). Sin la variable, ninguna respuesta lleva cabeceras CORS (deny-all). Un origin de la lista recibe access-control-allow-origin: <origin> en todas las respuestas de /styx/pkg/... (éxito y error) y el preflight OPTIONS (sólo GET/HEAD, cabeceras permitidas authorization y x-styx-request-id) responde 204; cualquier otro origen, 403 al preflight y ninguna cabecera CORS. Todas llevan vary: origin. Una entrada malformada impide arrancar el daemon.

ClosePackaging

{ "packagingId": "pkg_0f3c…" } → { "closed": true } (false si no existía).

Revoca primero (desliga el pkg_* de la autoridad) y después cierra: la siguiente petición con el SCT de esa sesión recibe 403. Los segmentos de esa sesión en vuelo por HTTP/3 se cancelan: en cola o produciéndose responden 503 Cancelled, enviándose se resetean.

Errores

Lo que viola el contrato (motor fuera de zig/libav/auto, segmentMs fuera de 1000..20000, un campo que falta o sobra, un tipo erróneo) no llega al handler: status = invalid_request con {"path","rule"} (antes MALFORMED_JSON, BAD_ENGINE, BAD_SEGMENT_LENGTH). Los errores de negocio van en { "error": { code, message, recoverable } }:

codeRecuperableCuándo
ENGINE_UNAVAILABLEnose pidió libav en un daemon sin libav (o motor no cableado)
ENGINE_NOT_ALLOWEDnose pidió libav para un fichero de una raíz sin :trusted (dec-0117 I6: libav en proceso sólo para raíces trusted); con auto esa raíz sólo usa zig
ASSET_FORBIDDENnorelPath fuera de su raíz (.., absoluta, symlink, otro montaje) o rootId desconocido (dec-0117 I7)
ASSET_NOT_FOUNDnobajo la raíz pero inexistente (la apertura fd-relativa lo distingue)
MALFORMED_MEDIAnocontenedor desconocido, corrupto, truncado o por encima de límites
UNSUPPORTED_MEDIAnoningún motor capaz para ese contenedor/codec, o pista pedida inexistente
CAPABILITY_BINDING_INVALIDnocapActor sin capResource o al revés
CAPABILITY_BIND_FAILEDsegúnsin autoridad, o el enlace no se pudo registrar (la sesión no queda)
TOO_MANY_SESSIONSsíSTYX_MEDIA_PACKAGING_MAX sesiones abiertas
READ_FAILEDsíE/S
CANCELLEDsícancelada: pasó el deadline_ms del sobre (o 30 s sin él), o el daemon se está parando (ZC7-P2-01)
OUT_OF_MEMORYsímemoria

Campos desconocidos, límites y seguridad

  • Campos desconocidos: los objetos del contrato son cerrados; un campo de más es invalid_request. Un campo nuevo es una versión de contrato nueva (protocols/schema-versioning/).
  • Límites: sobre de 8 KiB como mucho; relPath 1..4095 bytes bajo una raíz; segmentMs 1000..20000; sesiones abiertas ≤ STYX_MEDIA_PACKAGING_MAX; x-styx-request-id ≤ 64 bytes; los parsers de contenedor corren con presupuestos de lectura/memoria (bytes.Limits) y aritmética comprobada: un fichero hostil es MALFORMED_MEDIA, no un crash.
  • Plazo (ZC7-P2-01, como qry.media.probe desde ZC5-P1-01): un openPackaging corre como mucho hasta el deadline_ms de su sobre y nunca más de 30 s (engine_source.max_request_ms); pasado, cualquier lectura de cualquier motor falla y la respuesta es CANCELLED, con el hueco de STYX_MEDIA_PACKAGING_MAX devuelto. Un open en curso registra su token al tomar el hueco: la parada del daemon lo cancela antes de unir las conexiones. La conexión spire despacha sus sobres de uno en uno, así que este plazo es también lo más que espera un closeSession/closeIngest (revocación, dec-0117 I2) encolado detrás. El índice de keyframes de un Matroska sin Cues acota los frames que recorre (max_samples, ≤ tamaño del fichero), no sólo los keyframes.
  • Seguridad: sólo quien la tabla de rutas admite (playback-svc, styx-ops) abre o cierra empaquetados; sólo rutas bajo STYX_MEDIA_ROOT (apertura fd-relativa, O_NOFOLLOW, dec-0117 I3); cada petición HTTP/3 a /styx/pkg/... necesita el SCT de lectura de ese pkg_* (el packagingId, pkg_ + 128 bits CSPRNG, no es una credencial).

Configuración del daemon

VariableValoresDefecto
STYX_MEDIA_ENGINEzig/libav/autoauto
STYX_MEDIA_ENGINE_FALLBACKoff/libavoff
STYX_MEDIA_PACKAGING_MAX1..409632
STYX_MEDIA_PACKAGING_WORKERS1..642
STYX_MEDIA_PACKAGING_HELD_MIB529..65536768

Un valor inválido, o libav en un binario sin libav, para el arranque (no se degrada en silencio).

Probe (qry.media.probe, dec-0110 ME1)

Los facts de un asset por el motor de medios, en proceso (dec-0110 §1: FFmpeg sólo como librería). Emisores: catalog-svc (escaneo) y styx-ops. Sustituye al ffprobe que workers-svc lanzaba como proceso por cmd.workers.probe, retirado junto con su contrato.

{ "rootId": "media", "relPath": "film.mkv", "engine": "auto" }
CampoTipoReq.Descripción
rootIdstringsíId de la raíz en STYX_MEDIA_ROOTS ([A-Za-z0-9._-]{1,64}); una raíz desconocida es ASSET_FORBIDDEN (dec-0117 I7)
relPathstringsíRuta relativa a la raíz; se abre fd-relativa como createSession/openPackaging (.., absolutas, symlinks y otro dispositivo → ASSET_FORBIDDEN)
enginestringnozig | libav | auto; por defecto STYX_MEDIA_ENGINE. auto sigue la misma regla que openPackaging: Zig si lee el contenedor, si no libav cuando está compilado; un fallo de Zig sobre un contenedor que sí lee sólo pasa a libav con STYX_MEDIA_ENGINE_FALLBACK=libav. En una raíz sin :trusted libav no ve el fichero (dec-0117 I6): libav → ENGINE_NOT_ALLOWED, auto → el error de Zig

Respuesta:

{
  "result": {
    "engine": "zig",
    "container": "matroska",
    "fileSize": 3146594,
    "durationUs": 3000000,
    "streams": [
      {
        "index": 0,
        "kind": "video",
        "codec": "h264",
        "width": 320,
        "height": 240,
        "frameRateNum": 25,
        "frameRateDen": 1,
        "default": true
      },
      {
        "index": 1,
        "kind": "audio",
        "codec": "opus",
        "sampleRate": 48000,
        "channels": 2,
        "language": "eng",
        "default": true
      }
    ]
  }
}
  • container: mp4 | mov | matroska | webm. codec: el codec_name de ffprobe ([a-z0-9_], la tabla de facts de media-core ya lo alinea), unknown si el motor no lo reconoce. Sólo lo que el catálogo consume (IMediaStream).
  • Facts de un fichero hostil: cada valor se comprueba contra el contrato antes de salir; lo que no cabe se omite, nunca se trunca ni se redondea (language fuera de ^[A-Za-z0-9-]{1,16}$, enteros por encima de 2^53, fracciones de cadencia que no caben en u32). Un fichero con más de 64 pistas → TOO_MANY_STREAMS.
  • Errores de negocio: ASSET_NOT_FOUND, ASSET_FORBIDDEN (path guard), MALFORMED_MEDIA / UNSUPPORTED_MEDIA (el motor no lo lee), TOO_MANY_STREAMS, ENGINE_UNAVAILABLE (sin motor cableado o engine=libav sin libav), ENGINE_NOT_ALLOWED (engine=libav en una raíz sin :trusted), READ_FAILED, OUT_OF_MEMORY. catalog-svc los traduce a CATALOG_INVALID_PATH, CATALOG_PERMISSION_DENIED, CATALOG_MEDIA_UNSUPPORTED (permanentes) y CATALOG_PROBE_FAILED/CATALOG_PROBE_TIMEOUT (transitorios).
  • No abre sesión: el fichero se cierra antes de responder.