Aaron SawitSecurity engineer · Singapore

Write-ups · Root cause · 8 Sept 2026

Jellyfin, ffmpeg 7.1 and the HLS segment with no extension

Some live TV channels played in every other player and died in Jellyfin with a fatal player error. ffmpeg was refusing video segments because their URLs did not end in .ts.

What they said
One TV channel played everywhere except my media server.
What it was
ffmpeg 7.1 refuses video segments whose URL has no file extension.

Symptom

A free-to-air channel played in a browser and in VLC. In Jellyfin it failed with "Playback failed due to a fatal player error". The server log said FFmpeg exited with code 8 and Error probing live tv stream.

Finding the line

The playlist itself downloaded fine with curl, so the stream was up. The transcode log had one line that mattered:

[hls] URL https://.../stream-segment/6a288d...?d=I_53lLg... is not in allowed_segment_extensions

An HLS playlist is a text file listing short video segments. Most hosts name them segment-655.ts. This one served them from a path with no file extension. ffmpeg 7.1 added a safety check, extension_picky, that only fetches segments whose names end in a known media extension. It read the playlist, refused every segment, and exited.

The catch

ffmpeg has a switch for this. Jellyfin builds the ffmpeg command itself and has no field for extra input options. It also refuses a custom encoder path from its settings page when the container sets JELLYFIN_FFMPEG, which the official image does. The API answers 500 Unable to update encoder app path.

Fix

A twelve-line wrapper that Jellyfin calls instead of ffmpeg. It walks the arguments, and in front of every -i http... input it inserts the two options, then hands everything to the real binary in the original order:

#!/bin/sh
real=/usr/lib/jellyfin-ffmpeg/ffmpeg
n=$#; i=0
while [ $i -lt $n ]; do
  a=$1; shift; i=$((i+1))
  if [ "$a" = "-i" ]; then case "$1" in http*)
    set -- "$@" -extension_picky 0 -allowed_extensions ALL ;; esac; fi
  set -- "$@" "$a"
done
exec "$real" "$@"

A copy named ffprobe sits beside it, because Jellyfin derives the probe path from the same directory and the probe fails the same way. The compose file points JELLYFIN_FFMPEG at the wrapper.

My first version of the wrapper reordered the arguments. I caught it because I tested the wrapper by making it print its final argument list before I let it run anything.

Scope

The check exists for a reason, so the wrapper relaxes it only for network inputs on a server whose stream sources I choose. Local files are untouched.