Two libavcodecs behind one soname

KDE's Dragon Player has a habit of hiding the mouse cursor during playback. Not just in fullscreen — windowed too. Annoying enough to go looking for an alternative, which is how a missing cursor turned into a long argument with the dynamic linker.

Haruna is KDE's modern video player, built on libmpv. Qt6/KDE Frameworks 6, proper subtitle support, configurable keybinds. dnf install haruna, set it as default handler, open a 1080p HEVC file from the NAS.

No video track detected, most likely the video track can't be decoded/played due to missing codecs.

Fine. Except: RPM Fusion is enabled. libavcodec-freeworld is installed. ffmpeg -decoders | grep hevc shows five decoders. And mpv — the same mpv engine that Haruna embeds — plays the file from the command line without complaint. finished playback, success.

The GUI fails. The CLI works. They link the same library.

Every wrong answer I tried first#

Hardware decoding. The mpv trace showed hevc-vulkan ... Hardware decoding of this stream is unsupported?. That is mpv's Vulkan decode path, and VA-API failed too (hwaccel initialisation returned error): with Fedora's stock Mesa, neither can decode HEVC on this box (not the GPU's fault, see the footnotes). Fair enough — disable hwdec, force software. Except I set the wrong config key. I'd grepped the Haruna binary for HardwareDecoding, guessed it lived in [Video], and wrote:

[Video]
HardwareDecoding=no

Nothing happened. That string is the mpv property name — the thing Haruna passes to mpv_set_property(). The actual KConfig key is [Playback]HWDecoding. An expensive lesson in the difference between "strings the binary contains" and "keys the config file reads."

Network transport. The media lives on an SMB share. Maybe kio-fuse was mangling the file? Copied it locally. Same error.

HDR metadata. Maybe Dolby Vision side-data was tripping the tone-mapping shader? ffprobe showed plain bt.709 SDR. Not it.

Corrupt file. Full decode pass: ffmpeg -i movie.mkv -f null -, exit code 0, no errors. Clean.

GPU rendering path. Maybe Haruna's OpenGL context was somehow breaking the decoder? Ran mpv --vo=gpu, which played fine. Not quite the same path: mpv's own output came up on Vulkan here, while Haruna's libmpv renders through desktop OpenGL. So the renderer theory got less likely, not disproved.

Every hypothesis was reasonable. Every one came back clean. The method worked perfectly, and I was no closer to the actual bug.

Getting logs out of a GUI that won't give them to you#

Two things made this harder than it needed to be.

First, Haruna ignores ~/.config/mpv/mpv.conf. Completely. It uses KConfig — ~/.config/haruna/haruna.conf — and drives libmpv through mpv_set_property() calls, not config-file parsing. Every mpv.conf tweak I made was hitting a config the app never reads, and the app was too polite to mention it.

Second, Haruna's mpv logging is commented out in the source:

// mpvitem.cpp:143-144
//    setProperty(u"terminal"_s, InformationSettings::mpvLogging());
//    setProperty(u"msg-level"_s, u"all=v"_s);

The breakthrough came from reading the source. Haruna's MpvItem constructor (mpvitem.cpp:124-136) iterates over ~/.config/haruna/custom-commands.conf groups at startup — any group with Type=startup and SetOnStartup=true gets its Command string passed straight to libmpv. I wrote two debug groups:

[command_debug_1]
Command=set terminal yes
SetOnStartup=true
Type=startup

[command_debug_2]
Command=set msg-level all=v
SetOnStartup=true
Type=startup

This makes Haruna itself turn on mpv's terminal output and verbose logging. Launch Haruna from a terminal, and stderr finally has something useful.

This one is reusable: when a GUI app wraps a scriptable engine and hides the engine's logs, look for a config-driven escape hatch before you resort to rebuilding from source.

The four lines that explained everything#

[vd] Codec list:
[vd]     (no decoders)
Failed to initialize a decoder for codec 'hevc'.
[…]
[cplayer] finished playback, no audio or video data played (reason 4)

In Haruna's process, HEVC doesn't just fail to decode. It has no decoder at all. The codec list is empty, and mpv reports that in parentheses, like a footnote. Not "hardware decoding unsupported" — no software decoder either. The library that ffmpeg -decoders reports as fully loaded is not the library this process is using.

Two libraries, one soname#

Fedora ships codec-stripped libavcodec-free in /lib64/. No HEVC. Limited h264 via openh264. This is the base package — it satisfies the libavcodec.so.62 dependency for everything in the main Fedora repos.

libavcodec-freeworld from RPM Fusion is an add-on, installed to /usr/lib64/ffmpeg/. Full build — --enable-gpl --enable-libx264 --enable-libx265, every patent-encumbered codec included. Same soname: libavcodec.so.62.

Here's the mechanism, and it's in Haruna's own ELF header:

$ readelf -d /usr/bin/haruna | grep -E 'avcodec|RUNPATH'
 0x0000000000000001 (NEEDED)             Shared library: [libavcodec.so.62]
 0x000000000000001d (RUNPATH)            Library runpath: [$ORIGIN:$ORIGIN/../lib64]

Haruna links libavcodec directly, and it carries a RUNPATH: $ORIGIN/../lib64, which from /usr/bin is /usr/lib64, the directory with the free, HEVC-less build. ld.so searches a binary's RUNPATH before it consults /etc/ld.so.cache, so Haruna finds the free copy first and maps it. libmpv, loaded into the same process, asks for the same soname, and ld.so hands back the copy that is already mapped. First load wins, and nobody checks whether the first one was any good. No HEVC decoder registered.

mpv the CLI binary has no RUNPATH. Its lookup goes to the cache, which lists /usr/lib64/ffmpeg ahead of /usr/lib64. Freeworld loads. Full codec list. ldd shows the split (run on the same box after the fix below; RUNPATH and the cache order don't change with it):

$ ldd /usr/bin/haruna | grep avcodec
	libavcodec.so.62 => /usr/bin/../lib64/libavcodec.so.62 (0x00007f6ba6e00000)
$ ldd /usr/bin/mpv | grep avcodec
	libavcodec.so.62 => /usr/lib64/ffmpeg/libavcodec.so.62 (0x00007fcd9d000000)

Proof:

LD_LIBRARY_PATH=/usr/lib64/ffmpeg haruna movie.mkv

Decodes fine. LD_LIBRARY_PATH is searched before RUNPATH, so the freeworld directory wins, and Haruna gets the real library.

The fix#

The freeworld add-on can't help an application whose own RUNPATH points at the base library. You have to replace the base:

sudo dnf swap ffmpeg-free ffmpeg --allowerasing

This swaps the libav*-free stack for RPM Fusion's full ffmpeg build. The freeworld add-on stays installed, so there are still two copies of libavcodec.so.62, but the base one in /usr/lib64 now carries every codec too, and it no longer matters which copy a process maps first. No LD_LIBRARY_PATH hacks.

After the swap, Haruna's mpv logs (same custom-commands debug trick) show:

[vd] Codec list:
[vd]     hevc - HEVC (High Efficiency Video Coding)
[vd]     hevc_qsv (hevc) - HEVC video (Intel Quick Sync Video acceleration)
[vd]     hevc_v4l2m2m (hevc) - V4L2 mem2mem HEVC decoder wrapper
[vd]     hevc_amf (hevc) - HEVC AMD AMF video decoder
[vd]     hevc_cuvid (hevc) - Nvidia CUVID HEVC decoder
[vd] Opening decoder hevc
[vd] No hardware decoding requested.
[vd] Using software decoding.
[…]

Footnotes#

The hwdec thing was also real, but the GPU isn't to blame. With Fedora's stock Mesa, VA-API can't decode HEVC Main10 (hwaccel initialisation returned error), and 8-bit HEVC fails the same way. The Radeon 780M's video block does decode HEVC: the amdgpu kernel driver reports it up to 8192×4352. Fedora builds Mesa with the upstream default video-codecs=all_free, which leaves out the patent-encumbered codecs, so radeonsi offers VA-API only JPEG, VP9 and AV1. RPM Fusion's mesa-va-drivers-freeworld is the build with them: with it installed, mpv decodes a Main10 test clip on the GPU (Using hardware decoding (vaapi)). With real codecs present, mpv falls back to software cleanly. Before the swap, disabling hwdec just changed the error message from "hardware decoding unsupported" to "no decoders." There was no software decoder to fall back to.

xdg-mime only looks broken on Plasma 6. xdg-mime default org.kde.haruna.desktop video/mp4 prints qtpaths: command not found: on KDE it asks qtpaths (the Qt5 binary) for the config path, and Fedora 44 only ships qtpaths6. It still writes ~/.config/mimeapps.list and exits 0, and xdg-mime query default video/mp4 reads the new handler back. Ignore the error.

Read the source. Two guesses off the binary strings cost more time than the minute of git clone && grep -rn HWDecoding src/ that actually found the right config key. And reading the custom-commands model is what got me the logging escape hatch. The source is right there. It's faster than guessing, and I can say that with authority: I guessed twice.

Filed under: linux, fedora, kde, debugging

Sources#

  • Haruna source — v1.8.1, mpvitem.cpp startup commands and commented-out logging
  • RPM Fusion — ffmpeg-free vs ffmpeg package split
  • ld.so(8) — search order: LD_LIBRARY_PATH, then the binary's DT_RUNPATH, then /etc/ld.so.cache
  • Mesa meson.options — video-codecs defaults to all_free, the codecs that aren't patent-encumbered