Skip to content

Instantly share code, notes, and snippets.

@tohuw
Last active August 22, 2026 21:02
Show Gist options
  • Select an option

  • Save tohuw/9f15ac6b860c198f3c2c3184cc528c08 to your computer and use it in GitHub Desktop.

Select an option

Save tohuw/9f15ac6b860c198f3c2c3184cc528c08 to your computer and use it in GitHub Desktop.
Plex Desktop for Windows: HEVC playback stalls at position 0 with no error. Narrowed to the D3D11 hardware-surface handoff in Plex's bundled mpv 0.38.0 / libplacebo v6.338.2 - stock mpv of the same version renders fine in all 8 configurations. Includes the one-line workaround, benchmarks, and why swapping libmpv-2.dll cannot work.

Plex Desktop for Windows: HEVC playback stalls at position 0, forever, with no error

Conclusion: the fault is in Plex's build combination, not in any upstream component. Every upstream ingredient — mpv, libplacebo, the D3D11 hardware path, window embedding — works correctly when assembled the way upstream assembles it. Only Plex's assembly fails.

Workaround: one line, and it measures faster than the hardware path it replaces. Date: 2026-08-22


Symptom

Press play on an HEVC file in Plex Desktop for Windows. The UI says playing. The spinner never stops. The position never leaves 0:00. There is no error, no toast, no fallback to transcoding — it just sits there indefinitely.

Server-side it looks like a healthy session. The client reports a timeline every ten seconds, and every report says the same thing:

"state": "playing",  "time": 0,  "playbackTime": 632494

playbackTime climbs with wall-clock. time — the actual playhead — never moves. Across three sessions on one file I captured 128 consecutive timeline reports and every one had time=0.

The server is perfectly happy. It reaches a direct-play decision and serves bytes:

Streaming Resource: Reached Decision id=3861 codes=(MDE=1000,Direct play OK.)
Content-Length of .../Ultraviolet (2006).mkv is 5393459606

The client pulls 155 MB — exactly the mediaBufferSize=157286 (KB) Plex asks mpv for — fills its demuxer cache, stops reading because nothing is consuming frames, and the server eventually logs:

Failed to stream media, client probably disconnected after 155336704 bytes: 104 - Connection reset by peer

Bytes arrive, the demuxer fills, the renderer never draws a frame.


The fix

%LOCALAPPDATA%\Plex\mpv.conf:

hwdec=no

Restart Plex.

It costs nothing. Measured on the heaviest file in the library — 3840x2160 HDR10 HEVC Main 10 at 92 Mbit/s — decoded from a local copy on a Ryzen 9 9950X3D:

decode path fps vs. realtime
software (hwdec=no) 235 9.78x
d3d11va hardware 182 7.56x

Software is faster. Sixteen cores beat one NVDEC engine plus the readback. The only cost is some watts and CPU that was otherwise idle.

(Plex Desktop's Settings → Player → hardware acceleration toggle should be equivalent; mpv.conf is what was tested.)


Environment

Client Plex Desktop for Windows 1.115.0.426-4e960a1d
Client mpv 0.38.0, libplacebo v6.338.2, libavcodec 59.x (FFmpeg 5.1 era, patched)
OS Windows 11 26200
CPU Ryzen 9 9950X3D (16C/32T)
GPU RTX 5090, driver 32.0.16.1088
Display 3840x2160 @ 239 Hz, HDR on, 10-bit
Server Plex Media Server 1.41.5.9626 (not implicated)
Test file HEVC Main 10, 1920x1040, 10-bit, AAC 5.1, SDR, 7.6 Mbit/s

Swapchain mpv negotiates on that panel — note it builds an HDR PQ swapchain for SDR content, because the display is in HDR mode:

vo/gpu/d3d11: Swapchain successfully configured to color space RGB_FULL_G2084_NONE_P2020 (12)
vo/gpu/d3d11: Using flip-model presentation
vo/gpu:       Assuming 239.996000 FPS for display sync.
vo/gpu:       Reported display depth: 10

Where the fault is, and is not

The decisive comparison is Plex's own log, before and after hwdec=no:

hwdec on   ->  cplayer: VO: [gpu] 1920x1040 d3d11[p010]     ... no frame, ever
hwdec off  ->  cplayer: VO: [gpu] 1920x1040 yuv420p10
               cplayer: first video frame after restart shown   (125 ms later)

Same VO, same HDR PQ swapchain, same display, same file. Software frames present through that swapchain fine. So it is not the swapchain — it is the hardware surface handoff into it: a D3D11 p010 texture from d3d11va never becomes a presented frame.

Everything that was tested and cleared

Suspect Verdict Evidence
The file cleared ffmpeg decodes it in software and via -hwaccel d3d11va
Network / NAS / server cleared Direct-play decision reached, all ranges served, 155 MB delivered
Codec support cleared HEVC Video Extension installed; the GPU decodes HEVC Main 10 trivially
Multi-adapter (DisplayLink) cleared Window was on the discrete GPU throughout; pinning d3d11-adapter changed nothing
mpv version cleared stock 0.38.0 renders in 8/8 configurations (matrix below)
Every mpv option knob cleared colorspace-hint, swapchain-depth, output-format, gpu-next, vulkan, d3d11va-copy
swapchain-depth regression cleared mpv only reduced the default to 2 in 0.41; 0.38 already ran 3
Window embedding (--wid) cleared stock 0.38.0 embedded in a child HWND of another app renders with d3d11va
libplacebo v6.338 cleared stock 0.37.0 ships v6.338.0-62 — the same line Plex ships — and renders

The option matrix

Stock mpv 0.38.0 (the release Plex bundles), same machine, same panel, same file, --no-config, 12s each. Verdict = did mpv log first video frame ... shown:

baseline-d3d11va     RENDERS   [gpu] 1920x1040 d3d11[p010]   <- Plex's exact configuration
d3d11va-copy         RENDERS   [gpu] 1920x1040 p010
no-colorspace-hint   RENDERS   [gpu] 1920x1040 d3d11[p010]
output-fmt-rgb10a2   RENDERS   [gpu] 1920x1040 d3d11[p010]
swapchain-depth-1    RENDERS   [gpu] 1920x1040 d3d11[p010]
gpu-api-vulkan       RENDERS   [gpu] 1920x1040 cuda[p010]
vo-gpu-next          RENDERS   [gpu-next] 1920x1040 d3d11[p010]
hwdec-off            RENDERS   [gpu] 1920x1040 yuv420p10

The embedding test

Plex renders mpv into a child HWND inside its Qt/web-view application:

[MPVEngine] Property 'wid' set to '795608'

Every test above gave mpv its own top-level window, so this was the last structural difference. Reproduced it: a bare WinForms host window with a real message pump, mpv embedded via --wid, visible on the same HDR panel, frames confirmed on screen by eye.

control, hwdec=no        RENDERS   [gpu] 1920x1040 yuv420p10
mpv 0.38.0 + d3d11va     RENDERS   [gpu] 1920x1040 d3d11[p010]
mpv 0.37.0 + d3d11va     RENDERS   [gpu] 1920x1040 d3d11[p010]   (libplacebo v6.338.0)

Embedding is not the cause. Neither is libplacebo v6.338.


What is left: the build pairing

Build mpv libplacebo libavcodec embedded + d3d11va
Plex 0.38.0 v6.338.2 59.x — FFmpeg 5.1, patched STALLS
stock 0.37.0 v6.338.0-62 60.35 — FFmpeg 6.1 RENDERS
stock 0.38.0 v7.349.0 61.5 — FFmpeg 7.0 RENDERS

Each row shares components with Plex's. Only Plex's combination fails.

The one ingredient with no stock counterpart is Plex's FFmpeg. libavcodec 59 is the FFmpeg 5.1 line (mid-2022), and Plex patches it. D3D11 hardware frames are allocated by FFmpeg's libavutil/hwcontext_d3d11va.c; libplacebo then wraps those textures for rendering. If FFmpeg 5.1 allocates the texture array with different bind or misc flags than 6.x+ does, libplacebo's wrapping can fail — and "hwdec initialises, VO reconfigures, no frame ever, no error" is exactly what that looks like. No stock build pairs a v6.338 libplacebo with a 5.1-era FFmpeg, so the combination cannot be reproduced from off-the-shelf parts.

A secondary difference, less likely to affect presentation but worth naming: Plex does not use mpv's native stream layer. It registers its own via mpv_stream_cb_add_ro and feeds data through its own curl:

stream_callback: Opening https://...
curl: Opening https://...

Either way this is Plex's to fix, not upstream's. Every upstream component behaves correctly in its own context.


The remedy that cannot work: swapping libmpv-2.dll

Worth documenting because it looks viable right up until it isn't.

Plex ships mpv as a separate LGPL shared library, exactly as LGPL §6 relinking requires:

-Dcplayer=false -Dlibmpv=true -Dgpl=false -Ddefault_library=shared

So you are entitled to replace it, and the symbol surface checks out — all 54 of Plex's exports exist in a current build, none missing. Plex even starts on it:

Starting Plex version: 1.115.0.426-4e960a1d
[MPVEngine/mpv] cplayer: mpv v0.41.0-920-gdd5d17d32
[MPVEngine/mpv] cplayer: libplacebo version: v7.371.0

Then every stream fails to open with An unknown error occurred (4294967283) — that is -13 unsigned, MPV_ERROR_LOADING_FAILED. The reason:

cplayer: Setting option 'stream-lavf-o' = 'resolve_hosts=[192-0-2-10.<hash>.plex.direct:192.0.2.10]'
curl: error: Could not resolve hostname
stream: Failed to open https://192-0-2-10.<hash>.plex.direct:32400/video/:/transcode/universal/start

resolve_hosts is not a vanilla FFmpeg option. Plex patches FFmpeg with it so *.plex.direct hostnames resolve to the LAN IP encoded in the name without a DNS round-trip. Plex's libmpv-2.dll dynamically imports Plex's patched avcodec-59.dll / avformat-59.dll; stock libmpv builds statically link vanilla FFmpeg and ignore them. The option evaporates, real DNS is attempted, nothing opens.

This is also the clue that led to the conclusion above: the patched FFmpeg is not incidental packaging, it is load-bearing, and it is the component the working builds do not share.

(Back up first; restore with Copy-Item "$dst.backup" $dst -Force in an elevated shell.)


Diagnosing your own

Everything here came from two log files. No debugger, no special build.

Client: %LOCALAPPDATA%\Plex\Logs\Plex.log

cplayer: VO: [gpu] ...                     <- the pixel format names your path
first video frame after restart shown      <- absent = nothing was ever drawn

d3d11[p010] = zero-copy hardware surface. yuv420p10 = software. The presence or absence of that second line is the entire diagnosis.

Server: .../Plex Media Server/Logs/Plex Media Server.log — if time=0 repeats while playbackTime climbs, the client is lying about playing.

Is it your file? ffmpeg -v error -hwaccel d3d11va -i FILE -t 5 -f null -. A clean exit means the file and your decoder are fine and the problem is downstream of both.


Methodology notes

Three traps cost real time. All three produced plausible wrong answers rather than obvious errors, which is the dangerous kind.

1. Occluded windows invalidate presentation tests. The first option matrix launched mpv minimised, to be unobtrusive. A minimised or occluded window with a flip-model swapchain may legitimately never present, so every case reported "stalled" — including the known-good control. Any test of a presentation path needs a genuinely visible window.

2. A control case that must pass. Adding one is what caught trap 1 and trap 3 within seconds each. Without it, both would have been written up as findings.

3. Backslash escaping asymmetry. In this tooling the shell channel applied one extra round of backslash-unescaping and the PowerShell channel did not:

written    shell receives    PowerShell receives
\          \                 \
\\         \                 \\
\\\\       \\                \\\\

A UNC path written \\server\share arrived as \server\share, mpv silently failed to open the file, and a wrapper timeout reported that as a stall — the exact symptom under investigation. It produced a confident, completely wrong "stock mpv reproduces the bug" result that pointed at the opposite conclusion, and survived until the control caught it.

The general lesson: when your harness can produce the same observable as your bug, add a control that must succeed, and be most suspicious of the first result that confirms what you already believed.


For anyone reproducing this

The stall needs, as far as this investigation could establish: Plex Desktop for Windows, HEVC with hardware decode active, and a D3D11 flip-model swapchain. It reproduced on every HEVC title tried (34 of 42 in the library) and never on H.264. Whether the high-refresh HDR panel is necessary or merely incidental was not isolated — dropping the display to 60 Hz or turning HDR off, with hwdec re-enabled, would settle that and is the cheapest remaining experiment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment