Repository navigation
Releases: mugurc/jellyfin-plugin-pre-transcode
Release list
v0.11.1.0 — bug sweep: data safety, Apple playback, queue fixes
A bug-fix release, from a review of the whole plugin. If you use Replace in place, please update.
Which build is yours
| Your server | Use |
|---|---|
| Jellyfin 12.0 – 12.2 | this release — .NET 10, targetAbi 12.0.0.0 |
| Jellyfin 10.11 | 0.10.2.0 — the same fixes, built for 10.11 |
Installing from the plugin repository picks the right one for you.
Your files and settings
- Replace in place no longer destroys a file that changed during the encode. If Sonarr/Radarr imported an upgrade under the same name while the old release was encoding, the upgrade was overwritten by an encode of the old one — or, for mkv → mp4, deleted. The plugin now checks the source again right before it touches anything; if it changed or was deleted, the encode is thrown away and the file left alone.
- One stray character no longer wipes your whole configuration. A control character pasted into any text field saved fine, then made the file unreadable at the next start, and Jellyfin silently replaced it with defaults — every profile, rule and library override gone. Such characters are now removed on save, and if the file still can't be read at startup it is backed up first, with the recovery steps in the log.
- Jobs for a deleted profile no longer run under the default profile (which could be replace-in-place). They fail with a message saying so.
mpegtsand similar containers get extensions Jellyfin indexes (.tsinstead of.mpegts), so replacing a recording no longer drops it from the library. A profile whose output Jellyfin would not index is refused before encoding.- If the original can't be deleted after a replace (a player had it open), the job no longer fails and piles up
(1)copies on each retry. On case-insensitive disks (macOS, SMB shares, exFAT)GOPR0001.MP4is no longer replaced asGOPR0001 (1).mp4.
Playback
- Surround audio Apple devices dropped. With jellyfin-ffmpeg, AC-3/DTS 5.1 re-encoded to AAC came out with a channel layout Apple clients don't accept, so the track was silently missing on iPhone, iPad, Apple TV and Safari. It is now a standard 5.1. 6.1 becomes 5.1, because AAC has no seven-channel layout Apple plays.
- HEVC in MP4 is tagged
hvc1, which Apple clients need to direct-play.
Encodes that failed or were skipped
- Portrait phone videos always failed verification after a full encode.
- Odd frame sizes (720×303) failed in libx264/libx265.
- Text subtitles made every job fail for containers like mpegts and avi.
- H.264 High 10 and 4:2:2/4:4:4 files were skipped as "already compliant", although most devices can't play them.
Queue
- After a restart, waiting jobs started before Jellyfin had set its ffmpeg path, and failed with "could not probe" wherever jellyfin-ffmpeg isn't on PATH — Docker and Debian included.
- A server stop that also signals ffmpeg (systemd, Ctrl+C) recorded the running job as failed; three restarts locked the file out of automatic processing.
- Requeueing two records of the same file encoded it twice.
- A destination that can't be written (read-only mount, permissions, a name over 255 bytes) is found before encoding instead of after it.
- A new file arriving where a finished replace-in-place had been was skipped forever as "already transcoded".
Settings page
- Edits to the profile shown when the page opened were lost when you switched or added a profile.
- The default-profile choice reset when you added or deleted profiles or presets.
- Deleting a resolution preset lost other rows' edits, and could leave a profile pointing at nothing, which silently stopped downscaling.
- A failed save no longer leaves the spinner up forever.
Things you may notice
Containerconditions go by the file extension.movno longer matches.mp4files andmkvno longer matches.webm— ffprobe reports one name for both of each pair.matroskastill covers both.mkvand.webm.- Files that were wrongly treated as compliant (H.264 High 10, 4:2:2/4:4:4) will be queued once.
- Resolution caps treat portrait videos as portrait.
Verified
- 570 tests, run against Homebrew ffmpeg 9.0.2, ffmpeg 6.1.6 and jellyfin-ffmpeg 7.1.4, and in CI.
- Every Jellyfin API the plugin uses was checked against the 12.0.0 packages and the 12.2.0 server.
- On a real Jellyfin 12.2 server with jellyfin-ffmpeg 7.1.4: upgrading from 0.11.0.0 keeps settings and history; the restart failure reproduced with 0.11.0.0 and is gone with this build; a source swapped mid-encode was left intact; a damaged configuration file was backed up and recovered; 5.1, 6.1, portrait, 720×303, High 10 and subtitles-with-mpegts all encode, verify, play on Apple decoders and show up as versions.
Install
Plugin repository users: the update appears in Dashboard → Plugins after a refresh. Manual install: download pre-transcode-0.11.1.0.zip and unpack it into plugins/Pre-Transcode_0.11.1.0/.
Known, not yet fixed
Hardware-encoder specifics (#18), deinterlacing (#19), tone-mapping of HDR files with untagged primaries (#20), name collisions in separate-directory mode (#21) and history trimming defeating the retry limit (#22).
v0.10.2.0 — bug fixes for Jellyfin 10.11
The bug fixes of 0.11.1.0, built for Jellyfin 10.11 (.NET 9, targetAbi 10.11.0.0). If you use Replace in place, please update.
| Your server | Use |
|---|---|
| Jellyfin 10.11 | this release |
| Jellyfin 12.0 – 12.2 | 0.11.1.0 |
Installing from the plugin repository picks the right one for you: a 10.11 server is offered this build, a 12.x server 0.11.1.0.
What's fixed
The full list is in the 0.11.1.0 notes. The most important:
- Replace in place no longer destroys a file that changed during the encode — an upgrade imported under the same name by Sonarr/Radarr used to be overwritten or deleted.
- One stray character no longer wipes your whole configuration, and an unreadable configuration file is backed up before Jellyfin replaces it.
- Surround audio Apple devices dropped (AC-3/DTS 5.1 re-encoded to AAC with jellyfin-ffmpeg) is now standard 5.1.
- Jobs for a deleted profile no longer run under the default profile;
mpegtsoutputs are written as.ts. - Portrait phone videos, odd frame sizes and subtitles with mpegts/avi no longer fail; H.264 High 10 and 4:2:2/4:4:4 files are no longer skipped as compliant.
- After a restart, waiting jobs no longer fail because they started before Jellyfin set its ffmpeg path (Docker).
- Several settings-page fixes, including lost profile edits.
Things you may notice
Container conditions go by the file extension: mov no longer matches .mp4, mkv no longer matches .webm (matroska covers both). Files wrongly treated as compliant (H.264 High 10, 4:2:2/4:4:4) will be queued once.
Verified
- 562 tests on .NET 9 against Jellyfin 10.11, run against Homebrew ffmpeg 9.0.2, ffmpeg 6.1.6 and jellyfin-ffmpeg 7.1.4.
- Every Jellyfin API the plugin uses was checked against the real 10.11.11 server.
- On a real Jellyfin 10.11.11 server with its bundled jellyfin-ffmpeg 7.1.4: upgrading from 0.10.1.0 keeps settings and history; a portrait video that 0.10.1.0 failed now encodes; 5.1 audio plays on Apple decoders; alternate versions are grouped (including through the plugin's own database merge); a waiting job survives a restart without ffmpeg on PATH; a source swapped mid-encode was left intact.
Install
Plugin repository users: the update appears in Dashboard → Plugins after a refresh. Manual install: download pre-transcode-0.10.2.0.zip and unpack it into plugins/Pre-Transcode_0.10.2.0/.
Known, not yet fixed
Hardware-encoder specifics (#18), deinterlacing (#19), tone-mapping of HDR files with untagged primaries (#20), name collisions in separate-directory mode (#21) and history trimming defeating the retry limit (#22).
v0.11.0.0 — Jellyfin 12 support
Jellyfin 12 support.
Which build is yours
| Your server | Use |
|---|---|
| Jellyfin 12.0 / 12.1 | this release — built for .NET 10, targetAbi 12.0.0.0 |
| Jellyfin 10.11 | 0.10.1.0 — stays in the repository and keeps being offered to you |
You do not have to choose if you install from the plugin repository: a 10.11 server is not offered this build (it would not load there), and a 12.x server is offered this one first. Both were checked against real 10.11.11 and 12.1.0 servers reading this exact manifest.
What changed for Jellyfin 12
Nearly everything carried over untouched. The one part of the plugin that used an API Jellyfin 12 changed is Add as alternate version, which now links versions by item id rather than by file path, as 12 requires.
Alternate versions no longer wait five minutes on 12 (#15)
Jellyfin 12 recognises <source> - <label>.<ext> as a version of the source from its name — for episodes as well as movies — so the companion file shows up in the source's version selector straight away. The plugin used to wait five minutes for that file to appear as a separate library item, which on 12 it never does, and then log a warning telling you to merge it by hand. It now sees that the grouping is already in place and says so, usually within seconds of the encode finishing.
Also in this release
The container crop fix from 0.10.1.0 (#14): correct encodes of Matroska files with container crop metadata are no longer thrown away by the output check.
Already on 12 with 0.10.x?
It does load on 12 — .NET only resolves a changed API when it reaches it — so everything but one path keeps working. That path is the alternate-version merge, which fails on 12 and is logged as a warning. Updating to this release fixes it.
Verified
- 339 tests on .NET 10, locally and in CI.
- On a real Jellyfin 12.1.0 server, installed from this zip: loads and reports
Active; a UHD remux with container crop encodes to the correct 3840x1600 and is grouped as a second version of the source, with the merge reported as not needed 15 seconds after the encode. - The API members the plugin uses were checked by reflection against the
MediaBrowser.Controller.dllthe 12.1 server ships.
Install
Plugin repository users: the new version appears in Dashboard → Plugins → Catalogue after a refresh. Manual install: download pre-transcode-0.11.0.0.zip and unpack it into plugins/Pre-Transcode_0.11.0.0/.
v0.10.1.0 — container crop fix
A fix for correct encodes being thrown away after they had finished.
Matroska container crop (#14)
A Matroska file can ask for its picture to be cropped in the container rather than in the video stream — common on UHD remuxes, where 280 lines off the top and bottom turn a 16:9 frame into a 2.40:1 film.
ffmpeg 7.1 and later — including the one bundled with Jellyfin 10.11 — applies that crop by itself, so the encode came out right. But the check that runs before an output is kept compared it against the uncropped size, decided the shape was wrong, and discarded the file:
output verification failed: output shape does not match the source: 3840x1600 encoded from 3840x2160
…after however many hours the encode had taken, and on every file carrying this metadata.
- The crop is now read from the source, and the check accepts the picture as it is displayed as well as the uncropped frame — an older ffmpeg ignores the crop and produces the uncropped one, and that must keep working too.
- It still rejects what it exists to catch: embedded cover art encoded in place of the film.
- The displayed size is used everywhere else it matters: a cropped file that already fits a resolution cap is no longer queued for nothing, the
VideoWidth/VideoHeightrule conditions describe the picture you actually see, and a resolution cap no longer enlarges a cropped picture to reach a limit it was already under. - Files without crop metadata — nearly all of them — behave exactly as before.
Verified on a real Jellyfin 10.11.11 server with its own bundled ffmpeg 7.1.4, against a file with the reporter's exact geometry: 0.10.0.0 fails with the error above, this release completes and produces the correct 3840x1600 output.
Jellyfin 12
This release targets Jellyfin 10.11. Jellyfin 12 support is coming as 0.11.0.0; a 12.x server will not be offered this build, and a 10.11 server will not be offered that one.
Install
Plugin repository users: the new version appears in Dashboard → Plugins → Catalogue after a refresh. Manual install: download pre-transcode-0.10.1.0.zip and unpack it into plugins/Pre-Transcode_0.10.1.0/.
v0.10.0.0 — processing schedule + file-name search
Two requested features.
Daily processing window (#7)
Set Start processing at / Pause processing at on the plugin's settings page (Processing schedule), in 30-minute steps, in the server's local time. A download that lands at 19:00 no longer starts encoding while you are using the machine.
- Outside the window nothing new is claimed and a running encode is frozen exactly where it is at the OS level — no CPU used, no progress lost. Items are still evaluated and queued; the queue simply drains when the window opens.
- A stop time earlier than the start is an over-midnight window (22:00 → 06:00). Equal times mean all day, so ticking the box before choosing times can never stop the queue for ever.
- The control center now explains an automatic pause instead of just looking idle: it names the window and when processing resumes, and the job list greys a frozen encode the way a manual pause already did.
- Resume queue outside the window asks for confirmation and then runs until the window's next edge, after which the schedule takes over again.
- A manual pause still overrides everything and still survives a restart.
- Off by default; existing configurations are unchanged.
Single-item search now matches file names (#8)
Every word you type in Transcode a single item is matched against both the item's label and its full file path, so a show name, an S03E06, a 1080p or a release-group tag finds the file — alone or combined (someshow s03e06).
- Typing a series name lists its episodes, grouped and in season/episode order. This was impossible before: Jellyfin's own search only compares an item's own name, which for an episode is the episode title — never the show, never the path.
- Results are capped at 100, with a line saying how many matched in total.
Install
Plugin repository users: the new version appears in Dashboard → Plugins → Catalogue after a refresh. Manual install: download pre-transcode-0.10.0.0.zip and unpack it into plugins/Pre-Transcode_0.10.0.0/.
v0.9.0.0
Fixes a muxer error that killed jobs outright, adds hardware decoding, and fixes the queue's Pause on macOS.
Audio is negotiated against the output container
A track whose codec the output container has no tag for is now re-encoded instead of copied — the same treatment subtitles already had.
Copying an mkv source's TrueHD track into mp4, or an aac track into webm, made the muxer reject the output header, so the whole encode died within seconds of starting, on every attempt, until the job hit the failed-attempt limit.
Copy still means copy wherever the container allows it — all of Matroska, and the common case for mp4 — so profiles that already worked emit exactly the same ffmpeg command as before. A profile whose target audio codec the container cannot hold now falls back to one it can.
Hardware decoding (new "Hardware decoding" option, General)
ffmpeg can now decode the source on the GPU instead of the CPU. That is a large share of the work on 4K HEVC material, and it is worthwhile even when you encode in software — it is independent of the encoder your profile picks.
The list is built from what your own ffmpeg reports (-hwaccels). Off by default: that list says what the binary was built with, not what this machine can actually open. A method the host cannot open makes every job fail, so switch it on, run one job, and check the queue page before leaving it there.
Pausing the queue no longer wedges the worker on macOS
The encode was suspended but the call never returned, leaving a stopped ffmpeg that nothing could resume, cancel or kill. The signal is now sent directly rather than by running /bin/kill.
Also
- The end-to-end test suite no longer skips itself silently on machines whose ffmpeg is not in one of three hardcoded locations.
- A write-only field is no longer serialised into every queue-file write.
v0.8.0.0
Answers issue #5 and issue #6 — and closes a long list of defects a full audit of the plugin turned up along the way.
A control center, and a job list that scales (issue #6)
The plugin page fetched every job every two seconds and rendered a row for each one. On a library large enough to fill the queue with tens of thousands of jobs that is megabytes of JSON per poll and a table hundreds of screens long.
The main page is now a control center: the queue counts, what is encoding right now with its progress and ETA, the next few jobs, the last few that finished, and the existing sweep / pause / cancel controls. Open full queue and Open full history lead to a new page holding the whole list — paged, searchable by name or path, with Queue / History / All tabs. Nothing loads more than a screenful.
Finished records are still kept. "Clear finished" now says what it costs: a source with no record can be swept up and re-encoded again.
Replace in place no longer breaks playback (issue #5)
Replacing a file with a profile whose container differs from the source's extension renames it — Movie.mp4 becomes Movie.mkv — and nothing told Jellyfin. The database row kept naming the deleted file and every playback attempt failed with a file-not-found error until a library scan came round, which with real-time monitoring off (the default) can be a day away.
The item is now repointed at the new file the moment the swap is done, its container and size corrected in the same save, and its media info re-probed. Note that Jellyfin derives an item's id from its path, so its next full scan still re-creates the renamed item — keeping the container unchanged avoids that.
Data safety
- ffmpeg could encode the cover art instead of the film. The stream map matched attached pictures — embedded cover art — while the prober deliberately skipped them, so the two could disagree about which stream is the video. Where that happened the prober read the real track and matched a rule on it while ffmpeg encoded the poster thumbnail, and because verification only compared duration (which the audio track satisfies on its own) the result passed and, under Replace in place, the original film was deleted for it. In practice the ordering saves you: ffmpeg writes an mp4 attached picture as an iTunes
covratom, which is demuxed after the real tracks, and Matroska keeps cover art as an attachment — so on files from normal tools the old map already selected the right stream. This is a guard against unusually ordered files, not a description of something that was routinely happening. The map now excludes attached pictures, and verification also checks the output's shape and audio tracks. - Pause could freeze an encode with nothing left to resume it — SIGSTOPped for ever, holding a concurrency slot, with the queue reporting itself as running.
- A clean restart threw away a running encode, recording it as a user cancellation; it now goes back to Pending. Shutdown also waits for encodes to stop instead of leaving ffmpeg orphaned at full CPU, and the part-finished temp files that every cancel and crash used to leave behind — routinely several gigabytes, permanently — are swept at startup.
- The queue file is flushed to disk before it is renamed into place, so a power cut cannot take every Completed record with it.
Rules that silently did the wrong thing
Containerconditions are matched against what ffprobe actually reports (matroska,webm,mov,mp4,m4a,3gp,3g2,mj2).Container Equals mkvused to match no mkv at all, andContainer NotEquals mp4was true for every mp4 — so a rule written to catch what is not yet mp4 queued the whole library.AudioCodecandAudioChannelsare answered over every audio track instead of only the first. A remux whose commentary is muxed ahead of the main track was never queued. A positive operator means "some track is like this"; a negating one means "no track is". The explain log lists every track too.-presetis only passed to encoders that take one. Switching a profile fromlibx264tolibsvtav1carriedmediumover and killed every job before a frame was encoded.- An image subtitle Matroska cannot store (xsub, from AVI/DivX sources) no longer aborts the whole encode.
- An admin's own
-vfis merged with the plugin's filter chain instead of being silently discarded, and extra video args are no longer dropped entirely on a copy profile.
Performance
Persisting the queue rewrote the whole file on every change. Measured: 50,000 jobs is a 33 MB file and ~95 ms per save, with the queue lock held throughout — so a sweep enqueueing 5,000 items cost 7.9 seconds and 5.7 GB of disk writes, and both scale with the square of the queue size. Writes whose loss costs nothing are batched; a job reaching a final state, a claim and every admin action still write through. Same workload after: 104 ms and 2.3 MB. "Cancel all" got the same treatment.
Also in this release: the browser pages report a failed poll instead of silently showing stale figures as live, rows are no longer rebuilt under the pointer every two seconds (a click aimed at one row's Cancel could land on the next row's Remove), Remove asks first, and "x ago" in Recently finished no longer freezes once the queue goes idle.
v0.7.0.0
Answers issue #4 with a debug switch — and fixes four defects that switch made visible.
Explain every decision in the log
A new General option. With it on, every library item logs what ffprobe found, each trigger-rule condition with its configured value, the value it was compared against and whether it passed, and the reason anything was skipped:
Pre-Transcode evaluation of /media/Movie.mkv:
video=hevc 1920x1080 @ unknown (probe reported none) kbps, fps=23.976,
audio=aac/2ch, container=matroska,webm, duration=118 min, size=4021 MB
rule 'h265 too big (2hr)' (match ALL) => MATCH
VideoCodec In 'HEVC' | actual: hevc => PASS
VideoDurationMinutes GreaterThanOrEqual '100' | actual: 118 => PASS
FileSizeMb GreaterThanOrEqual '950' | actual: 4021 => PASS
verdict: at least one rule matched
Previously the evaluator had eight silent exits and only logged on success, so a rule that did not fire was undiagnosable.
Fixes
- A matched rule could be silently vetoed. The compliance check compares codec, container, resolution, audio and HDR — never file size or bitrate — so a profile meant to shrink material that already carries the target codec found every such file "compliant" and dropped it. Profiles can now switch off Skip files already matching this profile. On by default, so nothing changes for existing setups.
- A profile could write its output over its own source (separate-directory mode, no output directory, container matching the source's extension). The result was a
Movie (1).mkvbeside the original, which Jellyfin indexed as a second movie and the next sweep re-encoded intoMovie (1) (1).mkv— one more generation every sweep. Now refused with an actionable message, on both the sweep and the manual single-item path. - 10-bit sources were flattened to 8-bit, and HDR with tone-mapping off produced 8-bit samples still tagged as HDR — the one combination that is always wrong. The chosen encoder is now asked which pixel formats it accepts (they differ:
yuv420p10lefor libx265,p010lefor the NVENC/QSV/AMF families, none at all forh264_qsv) and the depth is kept for HEVC/AV1/VP9 targets and for untone-mapped HDR. H.264 still gets 8-bit — its 10-bit profile is not hardware-decodable on most clients. Configurable per profile. - Duration rules died silently on files whose container reports no format-level duration. The probe now falls back to the longest stream, then to the Matroska
DURATIONtag.
Also
- A non-media file now fails with
could not probe source fileinstead of a raw ffmpeg error code. - An alternate version Jellyfin already groups by filename is no longer linked a second time — on 10.11 that made the version picker list the transcode twice.
- Pausing the queue survives a server restart.
- Optional cap on retained finished jobs (0 = keep everything, the default).
Verified
219 unit tests, plus 44 scenarios run against a live Jellyfin 10.11.11 with real ffmpeg: all three output policies, the discard-if-larger and compliance-skip paths, five failure paths, job trimming, cancel, pause/resume (confirmed to actually suspend the encoder process), concurrency, seven rule-condition combinations through a per-library override, and the alternate-version merge.
v0.6.1.0
v0.6.0.0
On-demand single-item pre-transcoding
The Queue & status page now has a Transcode a single item panel:
- Search for a movie or episode by name.
- Pick which encoding profile to use.
- Queue just that one item — independent of the trigger rules and the automatic sweep.
It is still skipped at run time if the item is already compliant with the chosen profile, so you never waste an encode.
Fix: the queue list no longer looks frozen during a slow encode
On a large (e.g. 4K) file whose progress moves less than 1% between the page's 2-second polls, the row used to appear stuck — the progress bar, elapsed time and ETA only redrew when the whole-number percent changed. The list now keeps updating while any job is running (and shows progress to one decimal), while still preserving an open ffmpeg-log panel when nothing is processing.