Stream proxy¶
The stream proxy is a localhost HTTP server that runs inside NZB-DAV's background service. Every playback flows through it, and it's where seeking, format handling, and gap recovery happen.
Why a proxy exists¶
The proxy solves four problems that Kodi's native input streams can't on the target devices (Kodi 21, 32-bit CoreELEC/Amlogic):
- PROPFIND on the parent directory. Kodi PROPFINDs the parent folder before
a WebDAV file GET; on nzbdav this can trigger a recursive scan that throws
Open - Unhandled exception. A localhost server that answers only GET/HEAD sidesteps it. - MP4
moovat the tail. REMUX rips often place themoovatom after the media data; Kodi can't start those without a full download. The proxy rewrites them in pure Python. - 32-bit cache offset bug. On 32-bit Kodi, pass-through of a file with a large advertised size crashes at byte 0. The remux tiers hide the true size behind an unsized stream.
- Missing Usenet articles. nzbdav returns HTTP errors on unrecoverable ranges; the proxy catches these mid-stream and recovers.
How Kodi is handed a local URL¶
sequenceDiagram
participant S as Service process
participant HW as Home window props
participant R as Plugin process
participant K as Kodi player
S->>S: bind 127.0.0.1:0 (ephemeral port)
S->>HW: publish nzbdav.proxy_port + proxy_token
R->>HW: read port + token
R->>S: POST /prepare (X-NZBDAV-Token)
S->>S: build stream context, register session
S-->>R: local URL (/stream/{uuid} or /hls/{uuid}/playlist.m3u8)
R->>K: setResolvedUrl(local URL)
K->>S: GET the local URL (range requests)
The proxy binds an ephemeral port and publishes it plus a per-instance
security token to Kodi's Home window. The plugin process reads them and POSTs to
/prepare; the service builds the stream context (it owns the session table)
and returns the local URL. Only one session lives at a time — preparing a new
one tears the previous down, killing any ffmpeg process and cleaning its work
directory.
The serving decision tree¶
flowchart TD
P["/prepare"] --> FB{Fallback sources<br/>attached?}
FB -->|yes| PT[Pass-through context<br/>so cutover can run]
FB -->|no| MP4{URL ends .mp4/.m4v?}
MP4 -->|yes| L[Probe MP4 box layout]
MP4 -->|no| DEF{force_remux_mode<br/>+ size vs threshold}
L -->|faststart| PTM[Pass-through proxy]
L -->|moov at tail, parse ok| VF[Virtual faststart<br/>pure-Python stco/co64 rewrite]
L -->|parse fails| TF[Temp-file faststart if ffmpeg<br/>else MKV remux]
DEF -->|pass-through default| PT2[Byte pass-through]
DEF -->|matroska + big| MK[ffmpeg -c copy pipe]
DEF -->|hls_fmp4 + big| DV{DV route allows fMP4?}
DV -->|yes| HLS[fMP4 HLS VOD]
DV -->|no| MK
The classification order is: attached fallback sources force a pass-through
context (so cutover can run); otherwise .mp4/.m4v
URLs take the MP4 branch; everything else takes the default branch, where the
Large non-MP4 stream mode and the size threshold decide between pass-through
and a remux tier.
There is no direct-redirect tier
An already-faststart MP4 is proxied through /stream/<uuid>, not handed
back to Kodi as the remote URL. Kodi never receives the WebDAV URL.
The serving paths and seeking¶
| Path | ffmpeg? | Seeking |
|---|---|---|
| Pass-through (default for MKV/other, and for faststart MP4) | No | Native HTTP range seeking. |
Virtual faststart (tail-moov MP4) |
No | Native range seeking; stco/co64 offsets are rewritten in pure Python so 4 GB+ files work on 32-bit Kodi. |
Matroska remux (ffmpeg -c copy -f matroska pipe:1) |
Yes | No HTTP range; a seek past a 10 MB threshold respawns ffmpeg with -ss, so seeking is approximate to the nearest keyframe. |
fMP4 HLS (VOD playlist + init.mp4 + segments) |
Yes | Full random seek via 6-second segments; the playlist is generated by the proxy, and the canonical init segment survives seek respawns. |
Pass-through reads upstream in small chunks (heap-safe on 32-bit), and on a mid-stream read failure it skip-probes forward to the next readable offset and zero-fills the gap so the decoder keeps running.
Resilience knobs — exact behavior¶
These are read once per session. Defaults in parentheses.
| Knob | Behavior |
|---|---|
| Strict upstream contract mode (Warn) | Validates the upstream's status, Content-Range, and Content-Length. Off disables the density breaker entirely; Enforce treats a contract violation as fatal. |
| Density breaker (off) | When contract mode isn't Off, aborts the stream if a rolling 16 MB window becomes more than 50% zero-fill — i.e. the source has gone mostly synthetic (a dead release). |
| Zero-fill budget (on) | Caps zero-fill at 64 MB per response and 5% of the session; exceeding it ends the stream with a clean error rather than serving mostly-fake bytes. |
| Retry ladder (on) | Re-issues the original range with backoff on transient errors before skip-probing. A fresh open uses a short ladder so playback doesn't hang silently at the start. |
| Passthrough stall wait (120 s) | For an established stream that stalls on a recoverable backend condition (still-downloading or a transient 5xx), holds Kodi's connection open with abortable backoff up to this budget; the clock resets on any real forward byte. Doesn't apply to genuinely missing articles (those zero-fill) or a fresh open. |
| Read-ahead buffer (256 MB) | A bounded forward prefetch that keeps filling even while paused, so resume is instant; 0 is byte-for-byte identical to no buffer. |
| Send 200 for no-range (off) | Sends 200 OK instead of 206 when Kodi requests a full object. |
Dolby Vision routing¶
When fMP4 HLS is selected, NZB-DAV probes the first HEVC access unit for a Dolby Vision RPU (pure Python, no ffmpeg) and routes by profile, because DV over HLS hangs the decoder on Amlogic in several cases:
| Classification | Route | Reason |
|---|---|---|
| P7 FEL (dual-layer) | Matroska | fMP4 can't carry the enhancement layer |
| P7 MEL | fMP4 HLS | Metadata-only EL (experimental) |
| P5 / P8 / other DV | Matroska | Safest on Amlogic |
| Non-DV | fMP4 HLS | — |
| Unknown | Matroska | Fail-safe |
This is why force_remux_mode defaults away from fMP4: even with a correct DV
configuration record, DV HEVC over fMP4 HLS on CoreELEC-Amlogic can download
every segment and then produce zero frames, stalling around the 30-second mark.
The matroska pipe path doesn't trigger the code that causes it.
ffmpeg is optional¶
ffmpeg is discovered once at service start. The pass-through and virtual-faststart
paths never need it. When a tier wants remux but no ffmpeg is present, the proxy
falls back to pass-through with a warning. If ffmpeg is present but can't produce
a valid fMP4 init segment within 30 seconds, the session is rewritten to the
matroska path before Kodi ever sees the URL — so a broken HLS setup never
reaches the player. Credentials are passed to ffmpeg via an Authorization
header argument rather than embedded in the URL, so they don't leak into logs.
Next: how a failing source is swapped out live — Fallback cutover.