Hardware transcoding cannot be reached: the encoder is built from a hardcoded default, and three of its filter chains panic #94
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
crates/jellyfin-server/src/main.rs:160builds the encoder withEncodingOptions::default(). That default ishardware_acceleration_type: HardwareAcceleration::None(crates/jellyfin-encoding/src/options.rs:292), and nothing reads the value back out of/System/Configuration/encoding— so every transcode this server performs is software, on a machine whose GPU is idle, and no dashboard setting changes that.This is the single largest performance gap left. Every other number in
report.mdis a read-path number; transcoding is where a media server actually spends a processor.Three things have to happen, and they are separable.
The configuration has to reach the encoder. The
EncodingOptionsfields exist and are corpus-verified; what is missing is the wiring from the configuration section a dashboard posts to the value the arg builder is handed, and a re-read on a configuration change the way the rest ofsettings.rsalready re-reads.The seams that refuse have to be told apart from the seams that panic. M5's rule is that an unported hardware case refuses rather than answering, so that the corpus cannot pass a software arg string off as a hardware one. Four of them are
unimplemented!()rather than a refusal:crates/jellyfin-encoding/src/filter.rs:78(the filter chain for an unknown backend),:431(the AMD Vulkan/DRM interop chain),:466(QSV over d3d11va),:1275(hardware deinterlace), andcrates/jellyfin-encoding/src/quality.rs:83. In a test that is a refusal; in a server that is a panic in a request handler the moment an operator turns VAAPI on.The arg strings have to be run. VAAPI, QSV and NVENC are ported and agree with the corpus, and have never been executed — M5 named this as the empirical half that no amount of translation settles. The 324 AMF/VideoToolbox/RKMPP cases are not ported at all and are a runner problem rather than a porting one.
Verify by turning VAAPI on and playing a file that must transcode: today there is no way to do the first half of that sentence.
Where to start
Three separable pieces, in this order.
crates/jellyfin-server/src/main.rs:160is the whole of theproblem's first half —
EncodingOptions::default(). The struct and itsdefault are
crates/jellyfin-encoding/src/options.rs:220-330. Theconfiguration section a dashboard posts lands in
crates/jellyfin-http/src/routes/configuration.rs;crates/jellyfin-app/src/settings.rsis the existing example of a value that is re-read while the server runs
rather than read once at startup (M7 settled that this is a use case holding
state, not a port).
crates/jellyfin-encoding/src/filter.rs:78,:431,:466,:1275andcrates/jellyfin-encoding/src/quality.rs:83areunimplemented!().Each becomes a typed refusal a route turns into an answer, not a panic in a
handler. Keep M5's rule while doing it: an unported hardware case must refuse
rather than return
None, becauseNoneis what a machine with no hardwareanswers and the corpus would call that a pass.
MediaBrowser.MediaEncoding/Encoder/EncodingHelper.cs.crates/jellyfin-encoding/README.mdsays how the arg strings are known to beright; the cases are regenerated with
cargo xtask reference gen-corpusandnever hand-edited.
How to know it is done
cargo nextest run -p jellyfin-encodingstays green with a hardware backendconfigured — the corpus is the check on what is emitted.
unimplemented!/panic!and show each one is either unreachable or a refusal.and the spawned command line carries the hwaccel arguments. That is M5's exit
criterion repeated on hardware, and it needs a machine that has some — say
which one it was run on.
House rules
CLAUDE.mdis the short version and these are the parts that bite: tests rununder
cargo nextest; upstream's source is the checkout inupstream/at thepin (
v12.0-rc5,6d501ba4) and not the clone beside the repository;generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and
Dnreferences stay out of code and comments.ARCHITECTURE.mddecides which crate a thing goes in and which dependency edgesare illegal,
DECISIONS.mdsays why a rule exists.The reference pair is
cargo xtask reference up(andreference provision,which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,with
diff-dbfor the two databases.crates/jellyfin-harness/README.mdsayswhat a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.
Done, on
main— eight commits,e5892ebthrough7f0da07.A transcode actually ran on the GPU
The arg strings have been corpus-verified since M5 and never once executed. They have been now, against the pinned
jellyfin-ffmpeg 8.1.2— pulled out of the image withdocker cp, because/usr/bin/ffmpeghere is 9.0 and M5 settled that a stock binary answers differently about itself and earns arg strings no real install gets.Reproduced independently by a reviewer, who also confirmed
renderD128 → card1 → PCI 1002:744C. Then the live re-read, with no restart:POST /System/Configuration/encodingaddinghevcto the decode list → 204, and the next transcode carried-hwaccel vaapi -hwaccel_output_format vaapiandscale_vaapi=format=nv12:extra_hw_frames=24, at 44.4x.What the issue did not know
There were eleven panicking seams, not five.
hw.rscarried six more — the d3d11va child device, the input hwaccel dispatch, the hardware decoder dispatch, the d3d11va hwaccel, the Windows QSV device.grep -rn "unimplemented!\|todo!\|panic!\|unreachable!" crates/jellyfin-encoding/src/is now empty, and the two remaining.expect("graphical subtitle")calls were checked rather than assumed: each guard directly entails theSomeit unwraps.The refusals are typed (
Unported), neverOk(None)— M5's rule, becauseNoneis what a machine with no hardware answers and the corpus would then score software arguments as a passing hardware case.tests/args.rsno longer skips unported cases; it asserts they refuse.A probe was missing, and without it the wiring would have been a broken command line. With
vaapi_driver = Unknownevery branch ofvaapi_hwaccel_argsfalls through and emits no-init_hw_deviceat all. So turning VAAPI on would have reached the encoder and produced arguments that cannot work.capabilities::measurenow runs upstream'sCheckVaapiDeviceByDriverNameagainst the configured node; live here it readsMesa Gallium driver … navi31and reportsAmd.The corpus
Untouched —
git diffovercorpus/is empty. The run is 5,306 checked / 318 refused of 5,624, where 5,300 was the old checked count.324 hardware cases exist and only 318 refuse. The six that answer are all
-codec:v:0 copy: a remux decodes and encodes nothing, so no backend branch is on the way to its arg string. That exception was the one place the test was taught something new, so the reviewer tested the property across the whole corpus rather than those six — 31 copy groups, zero whose arg string differs between backends, zero mixing copy with non-copy — and structurallyinput_video_hwaccel_argsreturns early onis_copy_codecbefore any backend match. Not a hole.What the review changed
The save half-worked, which is worse than not working. The first pass kept five fields and answered 204 for the rest: posted tone mapping on, threads 4, CRF 30, preset slow — read back off, -1, 0, auto. An operator ticks Enable tone mapping, is told it saved, and every HDR transcode stays untonemapped. The whole section is persisted now, verified on a server built from this commit:
GETalso reported DTO zero-defaults for everything it did not map —H265Crf: 0where upstream reports 28 and our own options held 28. It reports 28 and 23 now.AMD_DEBUG=noefcwas not ported. Upstream sets it unconditionally on the AMD VAAPI branch, before the interop test, with its own comment that Mesa's EFC is unstable — so it applies to the limited chain this server takes. The corpus structurally cannot catch it: it is a process variable, not an arg string. This change is what made that path reachable, so it is exactly the divergence that goes live here.The Vulkan/interop divergence, stated properly
CheckVulkanDrmDeviceByExtensionNameis deliberately not ported. The reviewer ran upstream's probe on this card: all five extensions present. So upstream here setsIsVaapiDeviceSupportVulkanDrmInterop = trueand takes the libplacebo chain — which is unported — meaning probing it honestly would make VAAPI refuse every transcode on the only machine that can test VAAPI.What this server takes instead is the limited AMD chain, which is corpus-verified (
ffmpeg-amdexercises it). The first pass documented that as "an HDR source is tonemapped in software", which is wrong: it goes through OpenCL — a round trip out to memory and back, because Mesa has no VAAPI/OpenCL interop — and if OpenCL were absent the chain falls back entirely, so scaling and deinterlacing would not be on the device either. Slower than libplacebo on-device, not wrong. Both READMEs say that now, andcrates/jellyfin-encoding/README.md— the document that owns what green does not mean — carries it.No corpus environment has the extension (
vaapiSupportsVulkanDrmInterop: falsein all four), so this is a branch nothing sweeps, named rather than left to be found.DECISIONS.mdgains D29 for the refusal at the door: upstream saves any backend it is given, we answer 400 for one whose arguments we cannot build, because the alternative is a setting that saves and then fails every playback with nothing on screen naming the cause.Numbers
JELLYFIN_RS_REQUIRE_FIXTURES=1 cargo nextest run --workspaceonmain: 2188 passed, 0 skipped. Clippy clean.cargo fmt --checkproduces the identical hunk list to the tree before this change — the one hunk the first pass added is gone.Not established
PlaybackInfo, with no/Videosrequest issued at all and the renderer intermittently freezing — distinguishable from #132, which needs a segment request that is never made here. The server answers correctly at the point it stalls (Transcoding: true,SubProto: hls), and following that URL over HTTP transcodes end to end. The dashboard's Playback page does render the configured backend, the device, and the decode checkboxes including the one added over the API minutes earlier.HostOs::Windows, which is the best available and is not running them.Two minor and currently unreachable:
TranscodeError::Unsupportedbecomes a 500 on the two playback routes rather than the 400 the config route gives, andstart_or_joinextracts attachments and subtitles beforearguments()refuses. Both are guarded by validation at startup and at the route.