A Jellyfin plugin that limits the number of simultaneous streams per user.
Compatible with Jellyfin 10.11 (default build, net9.0) and Jellyfin 12 (net10.0 build), from the same source.
One manifest for every server version (Dashboard > Plugins > Repositories):
https://raw.githubusercontent.com/JellyboxAD/Jellyfin.Plugin.StreamLimit/main/manifest.json
Your server picks the right build automatically: Jellyfin 10.11 installs 1.1.0.0 (net9), Jellyfin 12 installs 1.1.1.0 (net10) — same features, one build per server generation.
⚠️ BETA — the HTTP-level hard block and the custom web message introduced in v1.1.x are new. They ship enabled by default and can each be turned off in the plugin settings to fall back to the classic behaviour.
403 before a single byte is served — works for every client (Swiftfin, Infuse, external players, custom apps), because a stream that is never served cannot be played. PlaybackInfo is answered with a proper "playback not allowed" error so clients show a clean dialog.
plugins folder
The settings page has three sections. Defaults are safe: install, set a limit, done.
Stream limits
Enforcement
PlaybackInfo) so clients fail cleanly before loading. Requires the hard block.Blocked message
DisplayMessage command. Only some native apps show it, and it adds a second popup on web clients.Upgrading from 1.0.x? Nothing to do: on first start the plugin migrates your existing configuration (legacy dashed user ids and corrupted entries are cleaned automatically).
Hard block (primary, default on, BETA). The plugin installs a global request filter inside the Jellyfin server. Every media request (progressive stream, HLS playlists and segments, universal audio, Live TV, /Items/{id}/File and /Download) is matched to a playback slot per user and device:
403 — no media bytes are ever served, so it cannot play no matter what client it is; blocked responses carry an X-StreamLimit: 1 header;PlaybackInfo returns ErrorCode: NotAllowed at the limit, so clients show a proper error dialog instead of a spinner;Web message (BETA). At startup the plugin adds a <script> tag to the web client's index.html (idempotent, reapplied after every server/web update) pointing at /StreamLimit/inject.js. That script detects a block (X-StreamLimit header or PlaybackInfo error), shows one popup with your configured title/text, and suppresses the client's own error dialogs — matching their structure, not their wording, so it works in every language. If the web folder is not writable, the plugin logs a warning and the client simply shows its built-in dialog: the block itself is never affected.
Reactive safety net. When a playback report still slips over the limit, the plugin stops the newest stream: stop command → transcode kill → optional server message → optional device logout. Paused streams count as active: they hold a playback slot.
Native clients. Swiftfin, AFinity, Streamyfin, Infuse and other native apps are fully blocked, but they display their own generic error screen — no server plugin can change or hide a native client's UI. Custom wording is only possible on web clients.
Management endpoints require elevated (admin) permissions:
GET /StreamLimit/GetUserStreamLimit?userId=<id>
→ { "userId": "<dashless-id>", "streamsAllowed": <effective>, "explicitLimit": <n|null> }
GET /StreamLimit/GetAllStreamLimits
→ { "defaultMaxStreams": <n>, "limits": { "<dashless-id>": <n>, ... } }
POST /StreamLimit/SetUserStreamLimit?userId=<id>&streamsAllowed=<n>
→ sets the user's limit; 0 removes it (the default applies again)
POST /StreamLimit/SetAlertMessage?alertMessage=<text>&title=<title>
→ updates the blocked-message wording
One endpoint is anonymous by design (it is loaded by the web client before login and contains no secrets):
GET /StreamLimit/inject.js → the web-message client script
User ids are accepted with or without dashes. Swagger UI: http://your-server/api-docs/swagger (StreamLimit section).
# Jellyfin 10.11 (default)
dotnet publish Jellyfin.Plugin.StreamLimit/Jellyfin.Plugin.StreamLimit.csproj -c Release -o publish/net9
# Jellyfin 12
dotnet publish Jellyfin.Plugin.StreamLimit/Jellyfin.Plugin.StreamLimit.csproj -c Release \
-p:PluginTargetFramework=net10.0 -p:JellyfinVersion=12.0.0-rc2 -o publish/net10
# Tests
dotnet test tests/Jellyfin.Plugin.StreamLimit.Tests/Jellyfin.Plugin.StreamLimit.Tests.csproj
The plugin has no external dependencies (System.Text.Json only), so the release zip contains a single DLL.
git checkout -b feature/AmazingFeature)git commit -m 'Add some AmazingFeature')git push origin feature/AmazingFeature)This project is licensed under the MIT License. See the LICENSE file for details.
For questions or issues:
32 commits
C#
85.2%
HTML
14.8%
A Jellyfin plugin that limits the number of simultaneous streams per user.
Compatible with Jellyfin 10.11 (default build, net9.0) and Jellyfin 12 (net10.0 build), from the same source.
One manifest for every server version (Dashboard > Plugins > Repositories):
https://raw.githubusercontent.com/JellyboxAD/Jellyfin.Plugin.StreamLimit/main/manifest.json
Your server picks the right build automatically: Jellyfin 10.11 installs 1.1.0.0 (net9), Jellyfin 12 installs 1.1.1.0 (net10) — same features, one build per server generation.
⚠️ BETA — the HTTP-level hard block and the custom web message introduced in v1.1.x are new. They ship enabled by default and can each be turned off in the plugin settings to fall back to the classic behaviour.
403 before a single byte is served — works for every client (Swiftfin, Infuse, external players, custom apps), because a stream that is never served cannot be played. PlaybackInfo is answered with a proper "playback not allowed" error so clients show a clean dialog.
plugins folder
The settings page has three sections. Defaults are safe: install, set a limit, done.
Stream limits
Enforcement
PlaybackInfo) so clients fail cleanly before loading. Requires the hard block.Blocked message
DisplayMessage command. Only some native apps show it, and it adds a second popup on web clients.Upgrading from 1.0.x? Nothing to do: on first start the plugin migrates your existing configuration (legacy dashed user ids and corrupted entries are cleaned automatically).
Hard block (primary, default on, BETA). The plugin installs a global request filter inside the Jellyfin server. Every media request (progressive stream, HLS playlists and segments, universal audio, Live TV, /Items/{id}/File and /Download) is matched to a playback slot per user and device:
403 — no media bytes are ever served, so it cannot play no matter what client it is; blocked responses carry an X-StreamLimit: 1 header;PlaybackInfo returns ErrorCode: NotAllowed at the limit, so clients show a proper error dialog instead of a spinner;Web message (BETA). At startup the plugin adds a <script> tag to the web client's index.html (idempotent, reapplied after every server/web update) pointing at /StreamLimit/inject.js. That script detects a block (X-StreamLimit header or PlaybackInfo error), shows one popup with your configured title/text, and suppresses the client's own error dialogs — matching their structure, not their wording, so it works in every language. If the web folder is not writable, the plugin logs a warning and the client simply shows its built-in dialog: the block itself is never affected.
Reactive safety net. When a playback report still slips over the limit, the plugin stops the newest stream: stop command → transcode kill → optional server message → optional device logout. Paused streams count as active: they hold a playback slot.
Native clients. Swiftfin, AFinity, Streamyfin, Infuse and other native apps are fully blocked, but they display their own generic error screen — no server plugin can change or hide a native client's UI. Custom wording is only possible on web clients.
Management endpoints require elevated (admin) permissions:
GET /StreamLimit/GetUserStreamLimit?userId=<id>
→ { "userId": "<dashless-id>", "streamsAllowed": <effective>, "explicitLimit": <n|null> }
GET /StreamLimit/GetAllStreamLimits
→ { "defaultMaxStreams": <n>, "limits": { "<dashless-id>": <n>, ... } }
POST /StreamLimit/SetUserStreamLimit?userId=<id>&streamsAllowed=<n>
→ sets the user's limit; 0 removes it (the default applies again)
POST /StreamLimit/SetAlertMessage?alertMessage=<text>&title=<title>
→ updates the blocked-message wording
One endpoint is anonymous by design (it is loaded by the web client before login and contains no secrets):
GET /StreamLimit/inject.js → the web-message client script
User ids are accepted with or without dashes. Swagger UI: http://your-server/api-docs/swagger (StreamLimit section).
# Jellyfin 10.11 (default)
dotnet publish Jellyfin.Plugin.StreamLimit/Jellyfin.Plugin.StreamLimit.csproj -c Release -o publish/net9
# Jellyfin 12
dotnet publish Jellyfin.Plugin.StreamLimit/Jellyfin.Plugin.StreamLimit.csproj -c Release \
-p:PluginTargetFramework=net10.0 -p:JellyfinVersion=12.0.0-rc2 -o publish/net10
# Tests
dotnet test tests/Jellyfin.Plugin.StreamLimit.Tests/Jellyfin.Plugin.StreamLimit.Tests.csproj
The plugin has no external dependencies (System.Text.Json only), so the release zip contains a single DLL.
git checkout -b feature/AmazingFeature)git commit -m 'Add some AmazingFeature')git push origin feature/AmazingFeature)This project is licensed under the MIT License. See the LICENSE file for details.
For questions or issues:
32 commits
C#
85.2%
HTML
14.8%