Plugin for Dispatcharr that stops an MKV "open-file burst" (2–3 near-simultaneous HTTP range
requests) from exhausting a max_streams: 1 VOD profile and failing over to a
different provider account / different underlying file. This has been observed and tested with Emby, but likely applies to other MKV direct-play players.
The plugin coalesces requests by (client IP, content): the first
request reserves one provider connection slot and the others within a few
seconds ride that reservation and stay pinned to the same provider/file,
instead of each being counted separately and rejected. Each request still opens
its own upstream range read. The plugin hooks both profile selection (so the
burst isn't rejected "at capacity") and reservation (so the slot is counted
once). See DESIGN.md for the full rationale and patch.py for the code.
- No configuration needed. No user data collected.
Targets current Dispatcharr's multi-worker VOD proxy. If the internals it patches change — or a request hits an unexpected error — it falls back to native behavior (no coalescing) rather than breaking playback.
- Download
dispatcharr_vod_concurrency_fix.zipfrom the latest release. - Dispatcharr UI → Plugins → Import → upload the zip.
- Toggle the plugin enabled (accept the trust warning — plugins run server-side code).
- Restart the Dispatcharr container (see "Why restart?" below).
- Clone or copy this repo into
data/plugins/dispatcharr_vod_concurrency_fix/on the host (→/app/data/plugins/…in the container). The folder must be nameddispatcharr_vod_concurrency_fixand containplugin.json+plugin.py. - UI → Plugins → click reload (or
POST /api/plugins/plugins/reload/). - Enable the plugin, then restart the container.
Dispatcharr runs 4 uWSGI workers with lazy-apps = true. Each worker imports an
enabled plugin's code at boot and applies the monkeypatch then. Enabling
without a restart only reliably patches the worker that handled the enable
request; a restart patches all of them.
- UI → Plugins → toggle off (Dispatcharr calls the plugin's
stop(), which reverts the monkeypatch in that worker and deactivates it in the rest). - For a clean, guaranteed revert across all workers, restart the container after disabling.
The plugin logs one line per worker per entry point the first time it runs:
[VOD-CC] installed VOD concurrency-coalescing patch in worker pid=<PID>
[VOD-CC] active in worker pid=<PID> at stream
[VOD-CC] active in worker pid=<PID> at reserve
Steps:
- After enabling + restarting, play a few different VOD titles (enough to hit multiple workers).
- Look at the Dispatcharr logs and collect the distinct
pid=values on the[VOD-CC] active … at reserve/… at streamlines. - You should see more than one distinct worker PID over several plays. If you only ever see one PID, the patch is not in every worker — restart again and re-check.
- Set the VOD profile's
max_streams: 1(the condition that used to fail). - Play the title that triggers Emby's burst.
- Before: the log showed
[PROFILE-SELECTION] All profiles at capacity …then[VOD-FAILOVER]to a second provider with a different Stream ID right after the first range request. After: you should see the coalescing lines instead, e.g.:Crucially: no[VOD-CC] group OWNER reserved profile 3 (account 7) for <ip>/<uuid> [VOD-CC] selection: reusing group profile 3 for <ip>/<uuid> (bypassing capacity, 1/1) [VOD-CC] group RIDER shares profile 3 slot for <ip>/<uuid> (members=2) [VOD-CC] selection: reusing group profile 3 for <ip>/<uuid> (bypassing capacity, 1/1) [VOD-CC] group RIDER shares profile 3 slot for <ip>/<uuid> (members=3) ... [VOD-CC] group member left profile 3, N remain (slot held) [VOD-CC] group LAST member -> releasing profile 3[PROFILE-SELECTION] All profiles at capacityand no[VOD-FAILOVER]for the burst, and playback stays on the correct file. Theselection: reusing group profile … bypassing capacityline is the one that proves the failover was prevented. - Afterwards, confirm the provider connection count returns to 0 (no leaked
slot) — the
group LAST member -> releasingline should fire once per burst.
Grep helper (adjust to your log access):
docker logs <dispatcharr-container> 2>&1 | grep -E "VOD-CC|VOD-FAILOVER|PROFILE-SELECTION|PROFILE-RESERVE|PROFILE-DECR"python test_logic.pySimulates the burst (selection bypass + rider), account pinning, different-client, partial-failure, and client-disconnect teardown — asserting the selection ladder and one-reserve / one-release symmetry. No Dispatcharr or Redis required.
The patch touches VOD streaming through the Xtream-Codes path
(stream_xc_movie/episode -> stream_vod), every profile, movies and episodes.
Live TV (live_proxy), the XC metadata endpoints, EPG, and DVR are not
touched.
-
Reading the log trace —
[VOD-FAILOVER]can appear benignly. When a burst request is pinned to the group's account, selection returnsNonefor other candidate accounts, and Dispatcharr logs[VOD-FAILOVER] Account X at capacity, trying next providerfor each skipped account — even though it was deliberately skipped, not truly full. This is only cosmetic as long as the request then lands on the group's account (you'll see[VOD-CC] selection: reusing group profile … bypassing capacityright after, and playback stays on the right file). The real failure signal is[PROFILE-SELECTION] All profiles at capacity … rejectingfollowed by a 503 / a switch to a different Stream ID. In the common case (the group is on your highest-priority provider) the loop picks it first and you won't see any[VOD-FAILOVER]for the burst at all. -
Coverage boundary. Only the XC path is coalesced. Requests that hit
/proxy/vod/...directly (some non-Emby clients) and HEAD requests run native — safe, just not coalesced. If your Emby is Xtream-Codes (the/movie/…,/series/…URLs), you're on the covered path. -
Account pinning trade-off. To keep a burst on one file, selection skips non-group accounts for the same (ip, content). If the group's account has its profile deleted mid-burst, that request falls back to native for that account and could 503 rather than failing over. Extreme edge (admin deleting a profile during playback); chosen deliberately over the alternative (silently drifting to a different provider/file).
-
Under-counting genuinely-separate playbacks that share (IP, title). Groups are keyed by
(client_ip, content_uuid). Two real separate playbacks that share both — e.g. two devices behind one public IP playing the same movie at once — merge into one provider slot. Each still opens its own upstream socket, so the provider sees 2 connections while Dispatcharr counts 1; if the provider enforces its own limit the second could be rejected. Near zero for a single-user homelab; possible on shared/NAT'd IPs. Watch for: a second device on the same title failing while the first works. Different titles from the same IP are unaffected (separate groups); different clients (different IPs) on the same title correctly still hit capacity. -
VOD stats UI may show more entries than provider connections — each burst request is its own session, so the panel can show N range readers for one logical playback while the provider connection count correctly reads 1. Cosmetic.
-
Leaked slot on abrupt crash — bounded and self-healing. If a stream dies skipping its teardown (worker crash, hard TCP reset), a group can hold its slot up to the group TTL (
GROUP_TTL_SECONDS, 30s); within that window a new same-(ip,content) request could bypass capacity onto the phantom group. Dispatcharr's own stale-connection cleanup still recovers the native counter (our decrement falls back to a raw native decrement outside a request context). No worse than stock, and scoped to one (ip, content).
Context safety: the (ip, content) context lives in a greenlet-local that
is set at the top of stream_vod and always reset at request end (the streaming
generator's finally, or the non-streaming/except paths). Combined with
uWSGI/gevent using a fresh greenlet per request, a prior request's context
cannot bleed into a later one.
vodcc:grp:{client_ip}:{content_uuid}— hash:refcount,reserved,profile_id,account_id,created_at,last_activity; TTLGROUP_TTL_SECONDS(30s), refreshed on activity. Deliberately distinct from the oldercedric-marcoux/dispatcharr_vod_fixplugin'svod_client_slot:keys.
Native keys (profile_connections:{id}, vod_persistent_connection:{session},
etc.) are untouched except through the unmodified native reserve/release calls.
The idea of coalescing a VOD client's near-simultaneous range-request burst by
(client IP, content) with a short grace period comes from
cedric-marcoux/dispatcharr_vod_fix
(MIT). This plugin is not a fork — it's an independent implementation for
current Dispatcharr, targeting different hook points (stream_vod,
_get_m3u_profile, _check_and_reserve_profile_slot,
_decrement_profile_connections), coalescing at both profile selection and
reservation, and using atomic Redis Lua with greenlet-local request context.
But that project was the inspiration for the approach, and credit is due.
Designed and built by andyj682 with Claude (Anthropic) as a pair-programming collaborator — Dispatcharr code analysis, concurrency design, and implementation.