Cap the resolution a Jellyfin user may be served
Quality Gate caps how tall a video a user can be served. The cap is measured against the media's real height, read from its video stream, so it holds whatever the file is called and survives a rename, a re-encode or a symlink.
Media above the cap is not hidden. It is served as a capped transcode, or as a lower-resolution version of the same item where one exists, and requests for the original file are refused.
It also, optionally, groups an encoded copy with its original so a single film shows two versions instead of appearing twice.
| Guide | What it covers |
|---|---|
| Getting started | Install, cap one user at 720p, and prove the cap holds |
| Installation | Every install method, upgrading, building from source, releases |
| Configuration | Every setting, how a user's policy is chosen, and the fields that do not restrict playback |
| How it works | The routes covered, the one that is not, and what to trust |
| One library, two qualities | Keeping a smaller encode beside each original |
| Troubleshooting | When it is not behaving |
Quality Gate began as a filename-pattern filter. Since 3.4.0.0 those patterns enforce nothing.
The port to Jellyfin 12 stopped registering MediaSourceResultFilter, the component that read
them. The fields are still in the config page and still saved, but no code consults them for
playback. The same goes for the fallback-transcode and blocked-message settings.
Maximum Resolution is the only setting that restricts playback. A policy that blocks
- 2160p by filename pattern and leaves the resolution at No limit restricts nobody. If you
are coming from 3.3.x or earlier, translate your patterns into a height.
Configuration lists them exactly.
- A measured resolution cap. 480p, 720p, 1080p, 1440p or 4K, checked against the item's video stream rather than its name.
- Both delivery paths. It shapes playback negotiation, and refuses the direct stream, HLS, universal and original-file routes that skip negotiation entirely. One legacy HLS segment route is a known, documented exception.
- Per-user policies. Assign policies individually, set a default, or mark a user explicitly unrestricted.
- Version grouping. Optional, movies libraries only. Groups
Film - 720p.mp4withFilm.mkvwherever the two sit, including a flat library root where Jellyfin would otherwise show two films. Applies to every movie library, or only the paths you list. Rebuilt on every scan, so it persists where a manual merge does not. - Per-policy intro videos. Optional. A different pre-roll for restricted users.
- Logging you can debug from. Every decision names the cap, the user and the policy.
Jellyfin 12, on net10.0. Version 3.4.0.0 and later will not load on Jellyfin 10.x; 3.3.6.0 is
the last build for 10.11 and is unmaintained.
Add this repository under Dashboard, Plugins, Repositories:
https://geiserx.github.io/quality-gate/manifest.json
Install from the catalogue, restart Jellyfin, then confirm the plugin shows Active. Full instructions, including manual installs, are in installation.
- Dashboard, Plugins, QualityGate.
- Add a policy, name it, set Maximum Resolution to
720p. Leave the rest alone. - Assign a user to it in the User Access table.
- Sign in as that user and play something in 1080p. Check Dashboard, Activity: the video being served must be at or below your cap. Both outcomes are correct, and which you get depends on the media. A capped transcode of the original, or direct play of a smaller version of the same item where one exists. Seeing "Direct playing" is not a failure by itself; being served something taller than the cap is.
Then verify it properly, because an unloaded plugin is indistinguishable from a working one that allows everything:
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Authorization: MediaBrowser Token="<capped-user-token>"' \
'https://your-server/Items/<item-id>/Download'403 is correct for an item above the cap. Run it as an unrestricted user too and confirm 200,
so you know the test can tell the two apart.
This is not DRM. Quality Gate shapes what the Jellyfin API delivers. Somebody with filesystem access, or a copy they already downloaded, is out of scope.
There is also no fallback outside the plugin. If it fails to load, every user is unrestricted and
Jellyfin reports nothing unusual. If you rely on the cap rather than on separate libraries, alert
on the plugin not being Active. Troubleshooting
covers a failure mode that does exactly that, silently.
Quality Gate is access control, so review your policies deliberately. Only administrators can configure them. Report vulnerabilities through SECURITY.md.
One behaviour worth knowing: deleting or disabling a policy that users are assigned to currently grants those users full access rather than removing it. Point users at a low-cap policy instead. Configuration explains why.
Pull requests are welcome. Fork, branch, and open a PR. CI has to be green and the patch covered. Installation has the build and test commands.
- smart-covers provides cover extraction for books, audiobooks, comics, magazines and music libraries, with online fallback
- whisper-subs generates subtitles locally using Whisper
- jellyfin-encoder does automatic 720p HEVC/AV1 transcoding, with optional symlinks for multi-version support
- jellyfin-telegram-channel-sync syncs Jellyfin access with Telegram channel membership
GPL-3.0. See LICENSE.
- Jellyfin, the Free Software Media System
- The Jellyfin plugin development community