Spec canónica de protocols/session-ipc/messages/media-engine.md, copiada sin reescribir.
protocols/session-ipc/messages/media-engine.mdPágina generada desde
protocols/session-ipc/messages/media-engine.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere.
cmd.media.openPackaging · cmd.media.closePackaging · qry.media.probeControl 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).
{
"rootId": "films",
"relPath": "film.mkv",
"engine": "auto",
"segmentMs": 6000,
"video": 0,
"audio": 1,
"includeAudio": true,
"capActor": "<32 hex>",
"capResource": "<64 hex>"
}| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
rootId | string | sí | Raíz de STYX_MEDIA_ROOTS ([A-Za-z0-9._-]{1,64}); contrato versión 2 (dec-0117 I7) |
relPath | string | sí | Ruta relativa a la raíz, 1..4095 bytes; se abre fd-relativa (sin ../absoluta/symlinks) |
engine | string | no | zig | libav | auto; por defecto STYX_MEDIA_ENGINE (otro valor: invalid_request) |
segmentMs | int | no | Duración objetivo de segmento, 1000..20000 (defecto 6000); corta en keyframes |
video | int | no | Índice de pista de vídeo; defecto = la primera. Si no existe: error, no sustitución |
audio | int | no | Índice de pista de audio; defecto = la primera. Si no existe: error |
includeAudio | bool | no | Defecto true |
capActor | string | no | Digest del actor (32 hex) al que se liga la sesión (dec-0117 I2); con capResource |
capResource | string | no | Digest 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.
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).
| Ruta | Content-Type |
|---|---|
/styx/pkg/<packagingId>/master.m3u8 | application/vnd.apple.mpegurl |
/styx/pkg/<packagingId>/index.m3u8 | application/vnd.apple.mpegurl |
/styx/pkg/<packagingId>/init.mp4 | video/mp4 |
/styx/pkg/<packagingId>/seg-<n>.m4s | video/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.
{ "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.
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 } }:
code | Recuperable | Cuándo |
|---|---|---|
ENGINE_UNAVAILABLE | no | se pidió libav en un daemon sin libav (o motor no cableado) |
ENGINE_NOT_ALLOWED | no | se 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_FORBIDDEN | no | relPath fuera de su raíz (.., absoluta, symlink, otro montaje) o rootId desconocido (dec-0117 I7) |
ASSET_NOT_FOUND | no | bajo la raíz pero inexistente (la apertura fd-relativa lo distingue) |
MALFORMED_MEDIA | no | contenedor desconocido, corrupto, truncado o por encima de límites |
UNSUPPORTED_MEDIA | no | ningún motor capaz para ese contenedor/codec, o pista pedida inexistente |
CAPABILITY_BINDING_INVALID | no | capActor sin capResource o al revés |
CAPABILITY_BIND_FAILED | según | sin autoridad, o el enlace no se pudo registrar (la sesión no queda) |
TOO_MANY_SESSIONS | sí | STYX_MEDIA_PACKAGING_MAX sesiones abiertas |
READ_FAILED | sí | E/S |
CANCELLED | sí | cancelada: pasó el deadline_ms del sobre (o 30 s sin él), o el daemon se está parando (ZC7-P2-01) |
OUT_OF_MEMORY | sí | memoria |
invalid_request. Un campo nuevo es una versión de contrato nueva
(protocols/schema-versioning/).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.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.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).| Variable | Valores | Defecto |
|---|---|---|
STYX_MEDIA_ENGINE | zig/libav/auto | auto |
STYX_MEDIA_ENGINE_FALLBACK | off/libav | off |
STYX_MEDIA_PACKAGING_MAX | 1..4096 | 32 |
STYX_MEDIA_PACKAGING_WORKERS | 1..64 | 2 |
STYX_MEDIA_PACKAGING_HELD_MIB | 529..65536 | 768 |
Un valor inválido, o libav en un binario sin libav, para el arranque (no se degrada en
silencio).
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" }| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
rootId | string | sí | 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) |
relPath | string | sí | Ruta relativa a la raíz; se abre fd-relativa como createSession/openPackaging (.., absolutas, symlinks y otro dispositivo → ASSET_FORBIDDEN) |
engine | string | no | zig | 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).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.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).Ingesta — cmd.media.openIngest, cmd.media.pollIngestEvents, cmd.media.closeIngest
Spec canónica de protocols/session-ipc/messages/ingest.md, copiada sin reescribir.
Session protocol v2 — control Bun ↔ daemon Zig sobre spire (dec-0120)
Spec canónica de protocols/session-ipc/SESSION_PROTOCOL.md, copiada sin reescribir.