Never serve a partial file: it makes Jellyfin transcode

The 12s first-byte grace was the wrong trade and a real play found it
within the hour. A 2-hour upload took 3 minutes to start, played 6
seconds, and stalled. Jellyfin had run ffmpeg with -probesize 1G against
the growing stream and then transcoded to HLS with libx264.

The cause is the container. A fragmented MP4 with empty_moov has no
duration in its header, so the only way to get one is to sum every
fragment -- probing a growing file reads all of it. Jellyfin cannot
establish duration, codec or bitrate, so it abandons direct play and
transcodes a stream it also cannot seek. It was targeting 4.83 Mbps
against a source measured at 3.29: re-encoding a stream that already fit,
because it could not measure it.

The same video once complete reports SupportsDirectPlay with the exact
runtime and bitrate.

So FIRST_BYTE_GRACE defaults to infinite again, with --wait-timeout raised
to 600s for a 2-hour upload. A cold long video is slow to start, which is
accepted: the fetch outlives the request so a retry is instant, and a
retryable stall beats a transcode that wastes a gigabyte and cannot work.
Both failure modes are recorded at the constant in the order measured so
the 12s cap is not reintroduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-13 11:32:12 +01:00
parent c12837cbb9
commit 22d8828080
10 changed files with 514 additions and 46 deletions
+10 -6
View File
@@ -21,11 +21,15 @@ UMask=0002
Environment=PATH=/var/lib/ytstream/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=PYTHONUNBUFFERED=1
# --first-byte-grace is the difference between "plays" and "playback error".
# Waiting for a complete mux costs ~3s to extract plus duration/50..110 to pull,
# so a 46-minute upload blocked this socket for 79 silent seconds and Jellyfin
# gave up long before. Do not raise this to --wait-timeout without re-reading
# the module docstring: that restores the behaviour that broke first plays.
# No --first-byte-grace: requests wait for the COMPLETE mux, bounded by
# --wait-timeout. Do not add a grace here without reading FIRST_BYTE_GRACE in the
# proxy. Short version, both measured 2026-08-13: waiting sends nothing for ~80s
# on a 46-minute upload and the client gives up, but serving the partial file
# instead makes Jellyfin drag a gigabyte through this proxy to probe a fragmented
# MP4 that has no duration in its header, and then TRANSCODE a stream it cannot
# seek -- 3 minutes to start, 6 seconds of video, then a permanent stall.
# A slow cold start is the better failure: the fetch outlives the request, so the
# retry is instant. --wait-timeout is 600 to cover a 2-hour upload (2.4 GB).
WorkingDirectory=/opt/ytstream
ExecStart=/var/lib/ytstream/venv/bin/python3 /opt/ytstream/proxy/ytstream_proxy.py \
@@ -35,7 +39,7 @@ ExecStart=/var/lib/ytstream/venv/bin/python3 /opt/ytstream/proxy/ytstream_proxy.
--max-pipelines 2 \
--max-retries 2 \
--max-starts 20 --starts-window 3600 \
--first-byte-grace 12
--wait-timeout 600
Restart=always
RestartSec=5