Files
ytstream/deploy/ytstream-proxy.service
T
Claude 22d8828080 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>
2026-08-13 11:32:12 +01:00

58 lines
2.1 KiB
Desktop File

[Unit]
Description=ytstream just-in-time YouTube streaming proxy
Documentation=file:///opt/ytstream/plan.md
After=network-online.target docker.service
Wants=network-online.target
# The bgutil POT provider runs in Docker on 127.0.0.1:4416 and yt-dlp cannot
# fetch anything without it.
Requires=docker.service
[Service]
Type=simple
# Group=mediaserver and UMask=0002 are load-bearing: Jellyfin reaches the media
# tree only through the mediaserver group. See plan.md §9.
User=susan
Group=mediaserver
UMask=0002
# LOAD-BEARING. The only yt-dlp with the bgutil POT plugin is the one in this
# venv. /usr/local/bin/yt-dlp is a 2023.11.16 binary that cannot talk to YouTube
# at all, and if it wins the PATH race every playback fails with no clear reason.
Environment=PATH=/var/lib/ytstream/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=PYTHONUNBUFFERED=1
# 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 \
--host 127.0.0.1 --port 8099 \
--work /dev/shm/ytstream \
--cache-gb 8 \
--max-pipelines 2 \
--max-retries 2 \
--max-starts 20 --starts-window 3600 \
--wait-timeout 600
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectControlGroups=true
ProtectKernelTunables=true
RestrictSUIDSGID=true
# /dev/shm is the cache; nothing else needs to be writable.
ReadWritePaths=/dev/shm
[Install]
WantedBy=multi-user.target