dec-0045

Vista generada de dec-0045: r32 — Bitrate NUNCA es gate de elegibilidad de direct-play

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0045-bitrate-not-a-gate.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0045-bitrate-not-a-gate.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.

CampoValor
EstadoLOCKED
Fecha2026-06-24
Referencia legadar32-bitrate-not-a-gate-2026-06-24
Ficherodocs/decisions/dec-0045-bitrate-not-a-gate.md

Nodos del roadmap que lo citan en refs: ninguno.

Páginas de la documentación que lo citan: ninguna todavía.

Texto del ADR

Leído de docs/decisions/dec-0045-bitrate-not-a-gate.md, el fichero canónico.

r32 — Bitrate NUNCA es gate de elegibilidad de direct-play

  • Fecha: 2026-06-24
  • Estado: LOCKED
  • Decisor: waxin (interview axon con previews, 2026-06-24)
  • Supersedes / matiza: r05 (PlaybackPlan), r20 §3 (capability model), r31 (reference-harvest win-conditions)
  • Provenance: docs/research/reference-harvest/ (Batch A+, CODE_VERIFIED contra clones reales)
  • Win-conditions cubiertas: WC-HIBR-01 (flagship 4K REMUX 80Mbps/0 cortes), WC-VBRPEAK-01

Contexto

El root cause #1 del "se corta a veces" de Jellyfin con un REMUX 4K de 80 Mbps, verificado contra el código (no inferido):

  • DeviceProfile.cs:30 → MaxStreamingBitrate = 8000000 (8 Mbps default server-side).
  • StreamBuilder.cs:712-727,1631-1655 → IsBitrateLimitExceeded → TranscodeReason.ContainerBitrateExceedsLimit fuerza transcode aunque el codec sea direct-play-capable. El default tira un REMUX de 80 Mbps a transcode sin tocar el slider.

Antídoto verificado: lunarr decidePlaybackMode decide solo por codec/container (transcoding/capabilities.ts:219-301, grep maxBitrate = 0 hits).

Honestidad obligatoria (matices del harvest a respetar)

  • El default 40_000_000 en StreamBuilder.cs:1643 es una sobre-estimación que FUERZA transcode cuando el bitrate real es desconocido, no un valor que "salta" el gate.
  • El gate de bitrate se salta para IsRemote (StreamBuilder.cs:1633-1636) — no es universal.
  • La cita lunarr correcta es capabilities.ts:219-301 (el :263+ original era erróneo; la sustancia es CODE_VERIFIED). Ver claims-register/downgrades.md.

Decisión (Opción A — codec-capability-only)

El PlaybackPlanner decide mode=direct ⟺ el cliente puede decodificar el codec/container. El bitrate NUNCA rehúsa direct-play. Es input explicable, jamás gate.

decideDirectPlay(asset, client, link):
  if !client.canDecode(asset.codec, asset.container):
     return adapted(reason: codec-unsupported)
  # bitrate NUNCA veta. availableBps entra como reason, no como gate.
  return direct(reasons: [
     codec-native,
     link.availableBps < asset.peakBps
       ? 'link-tight (prefetch deepens — ver r33)'
       : 'link-ample' ])

Reglas duras (load-bearing)

  1. No existe ninguna constante de bitrate que rehúse direct-play. Verificable por grep: ningún path del planner debe contener un maxBitrate/MaxStreamingBitrate que devuelva adapted/transcode por exceso de bitrate.
  2. bitrate-unknown = estado UNKNOWN de primera clase, no una constante mágica. Un asset con bitrate desconocido fuerza purpose=probe (medir antes de decidir), nunca se asume un default que dispare transcode.
  3. availableBps (medido vía cwnd QUIC, ref iroh-live util.rs:46-110) es un reason explicable en el plan, no un veto. Si el link va justo, la respuesta es prefetch más agresivo (r33), nunca rehusar direct-play ni bajar resolución.
  4. ClientCapabilities declara decode-capability (VideoToolbox HEVC Main10 / AV1 / DV profile), jamás MaxStreamingBitrate (matiza r20 §3).

Qué queda fuera de este ADR (delegado)

  • El comportamiento cuando el link de verdad no sostiene el envelope VBR → se resuelve en r33 (prefetch envelope-driven + closed-loop wait-pressure) como alerta/stall acotado, nunca downswitch. r33 está en review adversarial (ChatGPT) a fecha de este lock.

Consecuencias

  • Bate el Gate 1 de Jellyfin: un REMUX 80 Mbps con codec decodificable por el cliente va siempre direct-play. Es la precondición del flagship WC-HIBR-01 — sin esto, todo lo demás da igual.
  • El plan se vuelve explicable: reasons[] lleva el estado del link sin convertirlo en decisión de elegibilidad.
  • Empuja complejidad a r33 (si el link no sostiene, hay que prefetch/alertar, no degradar) — es un acoplamiento deliberado y aceptado.
  • NO se introduce ningún cap configurable por SourceBinding (Opción C descartada): es exactamente el footgun DeviceProfile.MaxStreamingBitrate que mató a Jellyfin.

Verificación al implementar

  • Test: REMUX 80 Mbps + cliente HEVC-capable → mode=direct, reasons incluye codec-native.
  • Grep adversarial: cero paths en el planner que devuelvan transcode/adapted por bitrate.
  • Test: asset sin bitrate conocido → UNKNOWN → purpose=probe, no transcode por default.