Probe defaults sit below upstream's, losing graphical subtitle geometry #14

Closed
opened 2026-08-02 14:27:23 +02:00 by Claude · 1 comment
Collaborator

prober::Settings is #[derive(Default)] with analyze_duration and
probe_size both None (crates/jellyfin-ffmpeg/src/prober.rs:38-50), so we
get ffmpeg's defaults — 5 s and 5 MB. Upstream passes
-analyzeduration 200M -probesize 1G, deliberate application defaults in
ConfigurationOptions.cs:18-19, overridable through FFmpeg:probesize and
FFmpeg:analyzeduration.

70 files probed both ways; 5 disagree, all in one way — the geometry of
image-based subtitles:

idx=2 hdmv_pgs_subtitle   cheap w=-    h=-      upstream w=1920 h=1080
idx=3 hdmv_pgs_subtitle   cheap w=-    h=-      upstream w=1920 h=1080
idx=4 hdmv_pgs_subtitle   cheap w=-    h=-      upstream w=1920 h=1080
idx=5 hdmv_pgs_subtitle   cheap w=1920 h=1080   upstream w=1920 h=1080

Upstream's own source says as much — EncodingHelper.cs:1295, "Use
analyzeduration also for subtitle streams to improve resolution detection"
.

Everything else matched on all 70: stream counts, codec, profile, level, pixel
format, channels, sample rate, frame rate, colour metadata, container tag count.

Where it lands. input.rs:236 emits -canvas_size {w}x{h} only when both
are present and positive, so a None silently drops the flag from the command
line; filter.rs:219-220 feeds the pair to graphical_sub_preprocess_filters,
the scale-and-overlay chain that burns a graphical subtitle in. For a Bluray rip
with PGS subtitles we build a different ffmpeg command line than upstream.

The arg builder itself is fine and the corpus exercises both branches — 402
graphical subtitle streams carry a width, 661 do not, and canvas_size appears
in 102 expected arg strings. The defect is that the scan writes NULL where
upstream writes 1920x1080, so two individually-correct paths get picked
differently.

config::Encoding has no field to configure this from.

Cost, measured cold-cache on 120 files, arms interleaved: cheap 0.102 s
mean, big 0.194 s mean — 1.90x. 18 % of files trip a "graphical subtitle with
no geometry" detector. Normalising to always-cheap = 1.0: always-big 1.90,
escalate-on-ambiguity 1.34. The hybrid is cheaper, but its detector can only
catch an absent field, never a wrong one — a cheap probe reporting 4 streams
where 8 exist looks well-formed. Worth doing only once the scanner is under a
diff that would catch it being wrong.


Found by scanning /mnt/media (285 movies, 289 shows, 74 music artists —
~13 000 media files on a CIFS mount) with both this server and upstream
Jellyfin at the pinned commit, remote metadata and image fetchers disabled on
upstream so both were reading files only. Upstream was given the same mount
path so MD5(UTF-16LE(path)) agrees and rows compare directly.

`prober::Settings` is `#[derive(Default)]` with `analyze_duration` and `probe_size` both `None` (`crates/jellyfin-ffmpeg/src/prober.rs:38-50`), so we get ffmpeg's defaults — 5 s and 5 MB. Upstream passes `-analyzeduration 200M -probesize 1G`, deliberate application defaults in `ConfigurationOptions.cs:18-19`, overridable through `FFmpeg:probesize` and `FFmpeg:analyzeduration`. 70 files probed both ways; **5 disagree**, all in one way — the geometry of image-based subtitles: idx=2 hdmv_pgs_subtitle cheap w=- h=- upstream w=1920 h=1080 idx=3 hdmv_pgs_subtitle cheap w=- h=- upstream w=1920 h=1080 idx=4 hdmv_pgs_subtitle cheap w=- h=- upstream w=1920 h=1080 idx=5 hdmv_pgs_subtitle cheap w=1920 h=1080 upstream w=1920 h=1080 Upstream's own source says as much — `EncodingHelper.cs:1295`, *"Use analyzeduration also for subtitle streams to improve resolution detection"*. Everything else matched on all 70: stream counts, codec, profile, level, pixel format, channels, sample rate, frame rate, colour metadata, container tag count. **Where it lands.** `input.rs:236` emits `-canvas_size {w}x{h}` only when both are present and positive, so a `None` silently drops the flag from the command line; `filter.rs:219-220` feeds the pair to `graphical_sub_preprocess_filters`, the scale-and-overlay chain that burns a graphical subtitle in. For a Bluray rip with PGS subtitles we build a different ffmpeg command line than upstream. The arg builder itself is fine and the corpus exercises both branches — 402 graphical subtitle streams carry a width, 661 do not, and `canvas_size` appears in 102 expected arg strings. The defect is that the scan writes NULL where upstream writes 1920x1080, so two individually-correct paths get picked differently. `config::Encoding` has no field to configure this from. **Cost, measured cold-cache on 120 files, arms interleaved:** cheap 0.102 s mean, big 0.194 s mean — 1.90x. 18 % of files trip a "graphical subtitle with no geometry" detector. Normalising to always-cheap = 1.0: always-big 1.90, escalate-on-ambiguity 1.34. The hybrid is cheaper, but its detector can only catch an *absent* field, never a *wrong* one — a cheap probe reporting 4 streams where 8 exist looks well-formed. Worth doing only once the scanner is under a diff that would catch it being wrong. --- Found by scanning `/mnt/media` (285 movies, 289 shows, 74 music artists — ~13 000 media files on a CIFS mount) with both this server and upstream Jellyfin at the pinned commit, remote metadata and image fetchers disabled on upstream so both were reading files only. Upstream was given the same mount path so `MD5(UTF-16LE(path))` agrees and rows compare directly.
Author
Collaborator

Fixed in a515afe and 85c29b7.

Verified against the real library: of 116838 streams both servers filed, not
one disagrees about its width, its height, its codec or its profile.

upstream ours
PGSSUB streams 3770 3770
— of those, carrying a size 3508 3508
shared streams differing on width or height 0

Both settings are configuration now, at upstream's own defaults —
analyze_duration = "200M" and probe_size = "1G" under [encoding] — and the
encoder is given them as well as the prober, which is the second place upstream
applies them.

The cost is real and as measured: the same library took 30 m 16 s against
14 m 29 s before, 2.09x. That is the price of the geometry, and of anything else
a stream only announces late.


Review found the answer keys were built in the wrong window, so 85c29b7
regenerates both:

  • the encoding corpus had every case generated with the two keys unset — command
    lines no server builds. All 5624 cases still build the string upstream
    builds, now with the flags on the front of each, which is what verifies where
    they are placed rather than merely that they are emitted.
  • the probe corpus recorded ffprobe's answer under five seconds while claiming
    to record what the server would have been given. Regenerated: the 32 files
    read the same either way, so only the recorded arguments changed — the fixture
    library holds nothing whose reading depends on how long it is looked at, which
    is why the fixtures never caught this.

The two scan tests that compare a probe against upstream's own snapshot were
probing cheaply and comparing against rows upstream probed expensively. They
probe the way it did now.

Two differences remain in MediaStreamInfos, neither of them this: 70 lyric
streams upstream files and this server does not, and one episode where the two
disagree about the stream set entirely.

Fixed in `a515afe` and `85c29b7`. **Verified against the real library: of 116838 streams both servers filed, not one disagrees about its width, its height, its codec or its profile.** | | upstream | ours | |---|---|---| | `PGSSUB` streams | 3770 | 3770 | | — of those, carrying a size | 3508 | **3508** | | shared streams differing on width or height | | 0 | Both settings are configuration now, at upstream's own defaults — `analyze_duration = "200M"` and `probe_size = "1G"` under `[encoding]` — and the encoder is given them as well as the prober, which is the second place upstream applies them. **The cost is real and as measured:** the same library took 30 m 16 s against 14 m 29 s before, 2.09x. That is the price of the geometry, and of anything else a stream only announces late. --- Review found the answer keys were built in the wrong window, so `85c29b7` regenerates both: - the encoding corpus had every case generated with the two keys unset — command lines no server builds. All **5624** cases still build the string upstream builds, now with the flags on the front of each, which is what verifies where they are placed rather than merely that they are emitted. - the probe corpus recorded ffprobe's answer under five seconds while claiming to record what the server would have been given. Regenerated: the 32 files read the same either way, so only the recorded arguments changed — the fixture library holds nothing whose reading depends on how long it is looked at, which is why the fixtures never caught this. The two scan tests that compare a probe against upstream's own snapshot were probing cheaply and comparing against rows upstream probed expensively. They probe the way it did now. Two differences remain in `MediaStreamInfos`, neither of them this: 70 lyric streams upstream files and this server does not, and one episode where the two disagree about the stream set entirely.
Sign in to join this conversation.
No description provided.