Vista generada de dec-0110: Motor de medios dual: demux/remux/packaging propio en Zig y libav (FFmpeg 8) como librería, configurable
docs/decisions/dec-0110-motor-de-medios-dual-zig-libav.mdVista generada desde
docs/decisions/dec-0110-motor-de-medios-dual-zig-libav.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.
| Campo | Valor |
|---|---|
| Estado | LOCKED |
| Fecha | 2026-09-28 |
| Fichero | docs/decisions/dec-0110-motor-de-medios-dual-zig-libav.md |
Enmienda a: dec-0039
Por qué importa (del frontmatter del ADR):
Gobierna cómo Styx hace demux, remux y packaging fMP4-CMAF/HLS: motor propio en Zig y motor libav (FFmpeg 8 como librería vía C interop dentro del daemon), intercambiables detrás de un contrato común
MediaEngine. Sin este ADR, un executor leería dec-0039/r07 («spawn ffprobe/ffmpeg = v1 sancionada») y añadiría más procesos ffmpeg, o escribiría el demux Zig sin un oráculo in-process con el que compararlo, o acoplaría el daemon a libav sin posibilidad de build sin FFmpeg (licencias).
Nodos del roadmap que lo citan en refs: track/byte-runtime/sec, track/media-engine, track/media-engine/audio-transcode
Páginas de la documentación que lo citan: Plugins y fuentes (implementado), Motor de medios y plan de reproducción (implementado), Planos y servicios (implementado), Transferencias (especificado), Modos de reproducción (implementado), Motores Zig y libav (especificado), Navegadores (especificado), Pistas y subtítulos (especificado), Visión (especificado), Primera película en Chrome (especificado)
Leído de docs/decisions/dec-0110-motor-de-medios-dual-zig-libav.md, el fichero canónico.
docs/track/byte-runtime/plans/vertical-vod-web.md):
§1.2 incluye también TrueHD (AC3/EAC3/DTS/TrueHD → AAC, encoder aac nativo y swresample,
todo LGPL). Se cierra con el gate item nuevo track/media-engine:ME7 (milestone
track/media-engine/audio-transcode).AskUserQuestion (2026-09-28): demux/remux/packaging fMP4-CMAF/HLS
PROPIO en Zig, con motor CONFIGURABLE entre el propio Zig y FFmpeg 8 (último) para comparar;
FFmpeg NUNCA invocado como proceso: sólo como librería (libavformat/libavcodec/libavutil) vía
interop C desde el daemon Zig (FFI).dec-0039 (spawn ffprobe/ffmpeg como v1 sancionada, derivado de r29-D3).r01 (Bun decide, Zig ejecuta; los bytes no pasan por JS), r03 (sin FUSE en hot
path), r04 (SeekableMediaSource, cancel(requestId)), r07 (orden FFI: daemon Zig por
socket > Node-API > bun:ffi), dec-0014 (C-ABI reservado a Zig↔libav), dec-0029 (crates
por capability: el placement de §1 respeta su taxonomía media-core / transmux +
libav-bridge / media-daemon sin cambiarla), r25 §2 (crates transmux + libav-bridge,
@cImport; su «CTransmuxFFI se consume tal cual» queda superado por el hallazgo de
licencia del harvest, §1.1), r31 D4 / dec-0043 (clean-room; d02: Styx MPL-2.0, provisional, LICENSE raíz), r16
(frontier), r23 (codec-agnostic, capabilities versionadas).dec-0109 (PROPOSED: facts en @styx/domain, parser en native/zig/media-core,
policy en @styx/playback-policy; spike zig-container-facts), dec-0112 (gate WC-JF12).dec-0113; renumerado a dec-0110 antes de merge al fijarse la
numeración final de las tandas 2/3: dec-0104 colas VOD · dec-0105 quic-zig upstream ·
dec-0106 ExtentMap · dec-0107 live/VOD · dec-0108 R-08 · dec-0109 placement mediakit ·
dec-0110 este ADR · dec-0111 client-core ABI v0 · dec-0112 gate WC-JF12 · dec-0113
identity token model · dec-0114 scanner por contenido · dec-0115 TypeBox vs Arktype
(SUPERSEDED) · dec-0116 contratos TypeBox + Elysia 2 beta (LOCKED) · dec-0117+ reservados
(spire/conduit/zkit). Nota del integrador de la tanda 3: la reserva original "0115 TypeBox,
0116+ spire/conduit/zkit" se desplazó uno al integrarse bench/typebox-vs-arktype, que ocupó
0115 y 0116.native/zig no tiene demux (ni moov, ni EBML/Cues), ni remux, ni
packaging fMP4/CMAF/HLS. Los facts de contenedor llegan por un único camino:
apps/catalog-svc/src/service/ffprobe/ffprobe-client.ts → cmd.workers.probe por NATS →
apps/workers-svc/src/service/ffprobe/ffprobe-client.ts hace Bun.spawn('ffprobe', …).r29-D3) sancionaba ese spawn como v1 (por r07: «FFmpeg como proceso,
no FFI de inicio») con migración a libav «prioritaria». dec-0014 ya reservaba el C-ABI para
Zig↔libav, y r25 §2 / dec-0029 ya nombraban el crate libav-bridge con @cImport.zig-container-facts (dec-0109, PROPOSED) mostró que un parser Zig clean-room de
ISOBMFF + Matroska da los mismos facts que ffprobe (688/703 iguales, 0 mismatches atribuibles
al parser, extradata byte a byte igual) leyendo ~0,9 % del fichero, en µs y sin proceso.dec-0112, SEEK-01 / DENSITY-01) depende de
no tener un proceso ffmpeg por sesión. Y para saber si la ventaja es de arquitectura o de
implementación del demux, hay que poder correr el mismo Styx con dos motores.MediaEngineMediaEngine vive en native/zig/media-core (módulo engine/), junto a los
tipos comunes. Es una interfaz Zig (vtable/union etiquetada) con dos implementaciones:
engine = zig — motor propio: demux ISOBMFF/fMP4 y Matroska/WebM, índice de keyframes,
remux a fMP4-CMAF. Zig puro, repartido entre media-core (demux) y native/zig/transmux
(mux fMP4-CMAF) según §1.1; el demux/facts reutiliza el parser que dec-0109 propone para
media-core (si dec-0109 se lockea con otro placement, el motor Zig lo sigue; este ADR no
decide ese placement).engine = libav — libavformat/libavcodec/libavutil de FFmpeg 8 enlazadas en el
daemon. Vive en un crate separado native/zig/libav-bridge (dec-0029, r25 §2), que es
el único sitio con @cImport de cabeceras de FFmpeg y el único que enlaza sus libs.probe(source, opts) → ContainerFacts — facts de contenedor y pistas (la forma de los facts
la fija dec-0109 / track/domain-media; el contrato sólo exige que ambos motores emitan la
misma estructura).openDemuxer(source, opts) → Demuxer con nextPacket(), seek(ts, flags),
keyframeIndex(); paquetes en un Packet común (pista, pts/dts en base de tiempo
racional, flag keyframe, datos como slice con dueño explícito).openRemuxer(selection, target = .fmp4_cmaf) → Remuxer con writePacket(),
flushFragment() → Fragment (bytes + metadatos: duración, keyframe inicial, rangos).packager HLS/CMAF (init segment, media segments, playlists m3u8, byte-range HLS): la
generación de playlists y la segmentación por tiempo son código Zig común a ambos
motores. Cada motor sólo aporta demux + mux de fragmentos. Así la comparación aísla lo que
difiere y no duplica lógica de delivery.requestId, r04/r20 §3.6) propagada a las lecturas de la
fuente; error set común (EngineError), sin filtrar códigos AVERROR fuera del bridge.r23): qué (contenedor, códec, target)
soporta cada motor. La selección las consulta; nunca se asume.SeekableMediaSource Zig: hoy media-core/source/local_file.zig con ReadRequest/
ReadPurpose, y el resto de fuentes cuando existan). libav recibe un AVIOContext custom
cuyos callbacks read_packet/seek llaman a esa fuente con el purpose correcto (probe,
playback, seek…). Prohibido pasar rutas de fichero o URLs a avformat_open_input: libav no
abre ficheros, sockets ni protocolos por su cuenta (r03, r04; además cierra superficie de
ataque de los protocol handlers de FFmpeg).dec-0029 / r25 §2, sin cambiarlo)Este ADR no mueve la taxonomía de crates de dec-0029 / r25 §2: la rellena.
| Crate | Qué pone este ADR ahí | Depende de |
|---|---|---|
native/zig/media-core | contrato MediaEngine (engine/), tipos comunes (Packet, Fragment, EngineError, capabilities del motor), facts de contenedor y demux Zig (ISOBMFF/fMP4, Matroska/WebM, índice de keyframes), fuente (source/) | — |
native/zig/transmux | remux Zig: loop de remux (selección de pistas, rebasado pts/dts, arranque en keyframe tras seek), muxer fMP4-CMAF propio (engine=zig) y el packager HLS/CMAF común a ambos motores (init/media segments, m3u8, byte-range HLS). Zig puro: nunca importa libav-bridge | media-core |
native/zig/libav-bridge | implementación engine=libav, clean-room: demux y mux de fragmentos con libavformat, transcodificación de audio a AAC con libavcodec (§1.2), AVIOContext custom sobre la fuente, av_log, manifiesto de licencia. Único @cImport de FFmpeg | media-core |
native/zig/media-daemon | compositor: registra los motores disponibles (libav sólo con -Dlibav=true), aplica la selección de §3 y expone los comandos IPC | los tres anteriores |
transmux no depende de libav-bridge: el packager consume Fragment del contrato, venga
del muxer Zig o del mux de libav. Así el daemon compila y empaqueta con -Dlibav=false, y la
comparación entre motores sólo varía demux + mux de fragmentos (§1, superficie común).r25 §2 asignaba a transmux
es exactamente el remux Zig de esta tabla; la diferencia es que aquí el loop es agnóstico del
motor (lee Packet del contrato), en lugar de ir atado a libav. Es clean-room, igual que
libav-bridge (ver el punto siguiente).CTransmuxFFI no se vendoriza ni se enlaza. TransmuxCore (y su capa C CTransmuxFFI, app
IPTV) es GPL-3.0 (docs/archive/research/reference-harvest/projects/mks-iptv.md: LICENSE
raíz GPL v3 + TransmuxCore/README), incompatible con Styx MPL-2.0 (r31 D4 / dec-0043,
d02). Por eso libav-bridge y el remux Zig de transmux son clean-room: TransmuxCore se
puede consultar como referencia de comportamiento (invariantes, state machines, recetas de
correctitud A/V como el delay_moov/frame_size de AC3/EAC3/DTS, fallos observados), con el
registro por idea que exige r31 D4; no se copia, porta ni vendoriza código ni structs.CTransmuxFFI es
sólo la capa C de acceso a FFmpeg (apertura, demux, codec, encoders). El remux a fMP4 fragmentado
creciente y la playlist HLS no viven ahí sino en Swift: TransmuxingService (loop
av_read_frame/av_interleaved_write_frame, fMP4 progresivo) y HLSSegmenter (parsea
moof/mdat y genera el m3u8). En Styx ese papel lo cubren el remux loop y el
packager Zig de native/zig/transmux (tabla de arriba), no ningún port.r25 §2 decía que CTransmuxFFI «se consume tal cual» (@cImport o compilar sus .c).
Esa premisa quedó superada por el hallazgo de licencia del harvest (mks-iptv.md, posterior
a r25): consumirlo tal cual metería código GPL-3.0 en un artefacto MPL-2.0. r25 no se edita
(registro de época); la corrección queda registrada aquí.docs/archive/research/2026-06-21-mksiptv-app-exploration.md). Decodificación con los
decoders de libavcodec y codificación con el encoder AAC nativo de FFmpeg (aac, LGPL).
Sin libfdk_aac (nonfree) y sin ningún componente GPL.libav-bridge (es libavcodec) y se declara como capability versionada del motor
(r23): con -Dlibav=false no existe. La policy la pide por pista de audio (acción ortogonal
por track, r05); engine=zig no la declara, así que una sesión que la necesite la resuelve la
selección de §3 por capability, nunca por suposición.ffmpeg, ffprobe o cualquier binario de FFmpeg como proceso desde
código de producto (apps/, packages/, native/), por spawn, exec, shell o sidecar. Esto
sustituye la sanción de dec-0039.workers-svc ffprobe-client.ts + el cmd.workers.probe que lo alimenta)
es legado a retirar: se reemplaza por un comando al daemon (probe vía MediaEngine) y se
borra (DELETE > @deprecated, regla de repos orquestados por axon). Hasta que aterrice el
reemplazo sigue funcionando, pero no se añaden call-sites nuevos. Un guard (grep/test) que
falle ante spawn('ffmpeg'|'ffprobe') en código de producto acompaña el borrado.media.engine = zig | libav | auto (default de arranque) y
media.engine.fallback = off | libav (sólo con auto).@styx/playback-policy, r01/r05) a partir de las capabilities de cada motor y la pasa en
el comando de apertura de sesión; el daemon valida que el motor elegido declara la capability y
ejecuta. Override por sesión permitido para benchmarks y diagnóstico.auto = motor Zig si declara la capability para (contenedor, códec, target), si no libav. El
fallback en caliente (Zig falla a mitad y se reintenta con libav) sólo existe si está
configurado, y nunca es mudo: contador por motivo, atributo en la traza, y visible en la
explicación del PlaybackPlan.media.engine en spans/métricas OTel)
y de cada corrida WC-JF12 (dec-0112 §4): toda corrida SEEK/DENSITY se hace con
engine=zig y engine=libav, además de contra Jellyfin.dec-0112, fijado por hash), ambos motores,
comparación a nivel de facts, paquetes (pts/dts/flags/hash de payload) y fragmentos
(hash de mdat, tablas trun/tfdt). Toda discrepancia se clasifica: bug del motor Zig, bug o
laxitud de libav, o diferencia legítima (documentada). Un canario (oráculo desplazado) debe
producir discrepancias, igual que en el spike, para probar que el harness discrimina.aac nativo que exige §1.2;
--disable-programs --disable-network --disable-autodetect), y enlazado desde
native/zig/build.zig detrás de una opción (-Dlibav=true|false). Con false el daemon se
construye sin FFmpeg y engine=libav no está disponible (capability ausente, no crash). Cómo se
integra la compilación (paso externo con prefijo, o paquete de build de Zig) lo decide el
milestone de build; el lock es: versión fijada, reproducible, y el daemon siempre compila sin él.--disable-gpl --disable-nonfree.
Demux, remux, packaging y la transcodificación de audio a AAC con el encoder nativo (§1.2) no
necesitan nada GPL. Tampoco entra código GPL por otra vía: nada de TransmuxCore/CTransmuxFFI
en el árbol (§1.1).configure, versión, libs) que el
daemon expone (p. ej. en Styx Doctor) para que el estado de licencia sea observable.av_malloc, fuera de los allocators Zig y de TrackingAllocator.
Hueco honesto: el consumo de libav se contabiliza por RSS/cgroup y por contadores propios del
bridge (paquetes y buffers vivos por sesión), y cuenta contra los presupuestos de memoria por
sesión. Contextos libav por sesión, nunca compartidos entre hilos sin sincronización;
av_log redirigido al logging del daemon.r16 (frontier, control e2e) y deja el hot path en código
C ajeno; el mandato es el motor propio, con libav como vara de medir y fallback.bun:ffi desde servicios TS. Rechazada por r01 (bytes no pasan por
JS), r07 y dec-0014.SeekableMediaSource, la
cancelación por request, la caché por niveles y amplía superficie de ataque.dec-0039 queda enmendado: el spawn deja de ser v1 sancionada; la migración ya no es
«prioritaria», es obligatoria y termina en borrado.media-core, se materializan los crates
transmux (remux Zig + muxer fMP4-CMAF + packager común) y libav-bridge que dec-0029 /
r25 §2 ya listaban (§1.1); el daemon gana un comando de
probe (y más tarde de sesión de remux/packaging) que sustituye a cmd.workers.probe.workers-svc pierde el executor de ffprobe; si se queda sin responsabilidad concreta, su futuro
se decide aparte (no aquí).-Dlibav=false y caché del build de FFmpeg.docs/track/byte-runtime/plans/media-engine-milestones-proposal.md (propuesta, no nodos).styx.model.yml; la propuesta es un plan a revisar en la
pasada de gobernanza.dec-0109 (placement de facts/parser/policy); sólo exige que ambos motores emitan
la misma estructura de facts.track/byte-runtime y los ADRs de transporte.dec-0039 lleva banner de enmienda y amendedOrSupersededBy: [dec-0110].r29 (D3, padre de dec-0039) no se toca: el matiz queda registrado vía dec-0039.dec-0112 (gate WC-JF12; engine es dimensión de las corridas), dec-0114
(scanner: la huella de contenido usa el probe de este motor).