Fallback cutover¶
This page explains how NeNeTeePee-Stream-Kodi switches to a backup source mid-playback without interrupting the video. For the user-facing summary, see Fallback streams.
Fallback cutover is part of the nzbdav backend only. The NZBGet backend bypasses this machinery and uses NZBGet's own duplicate handling. Playback of a remembered season-pack episode also runs without fallbacks.
Lifecycle¶
flowchart TD
PICK[You pick a source] --> SUB[Primary submitted + polled]
PICK --> DISC[Background candidate discovery<br/>result pool + Hydra re-uploads]
SUB --> PLAY[Playback starts]
PLAY --> WAIT[Wait: submit-delay seconds<br/>while playback stays live]
DISC --> WAIT
WAIT --> SUBMIT[Submit / adopt standby jobs]
SUBMIT --> PUSH[Push each source to the live session<br/>POST /stream/id/fallbacks]
PUSH --> PREV[Background prevalidation:<br/>content length + fingerprint sweep]
PREV --> STANDBY[Verified standby sources ready]
PLAY -. read fails .-> SELECT[Select a live fallback]
STANDBY --> SELECT
SELECT --> CUT[In-place cutover]
CUT --> PLAY
Candidate discovery starts in the background as soon as you pick a source, so
the Hydra and NZB-manifest lookups overlap the primary download. Nothing is
submitted, though, until playback is established: a stream you stop before the
submit delay elapses submits none, while steady playback submits its standby
backups once the delay passes. The submit worker waits for the playback-start
signal (giving up on the wait after 300 seconds and submitting late), then for
Seconds into playback before submitting fallback backups (default 120,
0 = at playback start). Opening extra backend connections during the fragile
startup window can starve the live stream. Every backup adopted after
/prepare is pushed into the live proxy session, where the prevalidation
warmer picks it up.
The related settings, Enable fallback streams (on) and Maximum standby fallback streams (default 5, hard ceiling 5), live in Advanced › Fallback Streams.
The "playing" liveness signal¶
The submit worker runs in the plugin process, but playback stop/end events fire
in the service process. They communicate through a cross-process Home-window
property, nzbdav.playing:
- It's set
truewhen playback monitoring begins. - It's cleared on stop, end, and terminal-error paths.
- A transient ERROR is not terminal. If a retry recovers, the flag stays set so backups still flow into the recovered playback.
The worker uses a "seen-live" latch: it only stops when the flag goes false after it has positively observed playback live at least once, which prevents a startup race from canceling backups prematurely.
Choosing candidates¶
NeNeTeePee-Stream-Kodi admits a candidate only when all of these hold:
- It has a different download link from the primary, and its NZB article set isn't the primary's (the same upload listed twice is not a backup).
- It's the same content (title, year, part, season/episode, edition, proper/repack, upscaled).
- It's from the same release group at the same resolution. Both must be parsed; an unknown group or resolution fails closed. Other parsed profile fields (HDR, audio channels) must agree too.
- It isn't a dead candidate.
NeNeTeePee-Stream-Kodi orders admitted candidates with exact-same video filename first, then by tier, then by smallest size difference. Admission already guarantees the same group and resolution and rejects a known codec mismatch, so the tiers come down to:
| Tier | Criteria |
|---|---|
| 0 | Same codec; size within 3% |
| 1 | Same codec; larger size difference |
| 2 | Codec not parsed on one side |
NeNeTeePee-Stream-Kodi collapses reposts of the same release within the same hour to the best-ranked survivor (anchor-based, no transitive merging), and drops any candidate posted within that window of the primary's own post date. It then clamps the count to Maximum standby fallback streams.
Verifying a switch is safe¶
Switching only works if the alternate's bytes line up exactly, so NeNeTeePee-Stream-Kodi checks this in two sampled stages (strong evidence of identical bytes, not a proof of every byte):
- Content-length equality. The alternate's total size must exactly equal the current source's. A mismatch permanently rejects that candidate.
- SHA-256 fingerprint sweep. NeNeTeePee-Stream-Kodi compares hashes of matching 4 KiB byte ranges sampled deterministically across both files: 20 samples for files under 1 GiB, 100 for larger files, with the first and last ranges always included. Every sampled range must match. A missing or empty hash on either side is inconclusive rather than a match.
NeNeTeePee-Stream-Kodi verifies standby sources ahead of time on a background thread, so a live cutover usually skips straight to verifying just the current range.
The cutover sequence¶
sequenceDiagram
participant K as Kodi
participant PX as Proxy serve loop
participant SEL as Fallback selector
participant OLD as Failing source
participant NEW as Backup source
Note over PX: streaming primary at byte st.current
OLD-->>PX: read fails (missing articles / upstream error,<br/>or 3 no-progress still-downloading reads)
PX->>SEL: select live fallback at st.current
SEL->>SEL: skip failed sources
loop each candidate
SEL->>NEW: content-length gate
alt already validated
SEL->>NEW: probe current range only
else
SEL->>NEW: full fingerprint sweep
end
end
alt MATCH
SEL->>PX: activate fallback
PX->>OLD: demote to standby (validated)
PX->>PX: swap remote_url + auth, reset window counters
PX->>NEW: next upstream read at st.current
NEW-->>PX: bytes
PX-->>K: same response continues (toast: "fall back to candidate #N successful")
else MISMATCH
SEL->>PX: mark source failed (permanent)
else INCONCLUSIVE
SEL->>PX: bump transient-miss count, abandon on the 5th in a row
end
The key detail: the cutover doesn't change the byte offset. The serve loop is
sitting at st.current; after the swap it re-enters the same loop at the same
offset on the same client socket, with the response headers already sent. There
is no Player.Stop, no rewind, and no re-sent HTTP headers, which is why the
switch is invisible.
When a switch succeeds, the proxy demotes the dead primary into the standby pool and marks it validated (it was, after all, serving these exact bytes a moment ago), so it could even serve later ranges if a subsequent source fails.
If the new source never delivers a byte before the proxy moves on, the toast reports that candidate as a failure instead ("fall back to candidate #N was a failure").
When nothing can recover¶
The proxy tries a cutover when a read hits missing articles or an upstream error, or after three consecutive still-downloading reads at the same offset that make no progress. If no verified source can take over at the exact byte position, the proxy doesn't hard-close. It re-enters the primary's retry ladder, so a primary that is only briefly trickling can still recover. The normal skip-probe and zero-fill budgets bound the damage.
That fall-through is bounded. After three consecutive fall-throughs that
stream no new real upstream bytes, the proxy closes the stream cleanly with
fallback_exhausted. Any genuine forward progress resets the count. The proxy
reports a terminal 401/403 or contract mismatch as itself instead of as
fallback_exhausted.
Dead-candidate tracking¶
NeNeTeePee-Stream-Kodi remembers candidates that are provably unrecoverable for the session (a missing first article, an NNTP rejection, or a terminal Failed/Deleted state), keyed primarily by the indexer download link (the only id stable across resubmits). Those are never retried. A timeout is deliberately not treated as dead: on a slow backend a timeout means load, not a missing post, so the candidate stays eligible.