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>
58 lines
2.1 KiB
Desktop File
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
|