Jellyfin MPV Shim is a cross-platform client for Jellyfin. It can run in the background as a cast target or act as a fully-featured desktop client with offline sync support.
It has support for all your advanced media files without transcoding, as well as tons of features which set it apart from other multimedia clients:
To learn more, keep reading. This README explains everything, including configuration, tips & tricks, and development information.
If you are on Windows, simply download the binary. If you are using Linux, you can install via flathub or install via pip. If you are on macOS, see the macOS Installation section below.
To use the client, simply launch it and log into your Jellyfin server. You’ll need to enter the
URL to your server, for example http://server_ip:8096 or https://secure_domain. Make sure to
include the subdirectory and port number if applicable. You can then cast your media
from another Jellyfin application. You can also use quick connect, which is easier and faster.
The application runs with a notification icon by default on Windows and if installed on Linux. If you would like the application to run in the background without the notification icon, enable it in settings.
Note: Due to the huge number of questions and issues that have been submitted about URLs, I now tolerate
bare IP addresses and not specifying the port by default. If you want to connect to port 80 instead of
8096, you must add the :80 to the URL because :8096 is now the default.
Please note the following issues with controlling SyncPlay:
Music playback works, but gapless playback is not planned at this time.
The shader packs feature is sensitive to graphics hardware. If the application fails to launch, use
--reset-shaders command line argument to reset the shader configuration. If it's merely garbled graphics
you can press k to disable the last used shader config.
Most of these are also reachable from the player UI's settings (gear) menu, which is usually easier. This menu is the older text-based one, and it still covers a few things the player UI does not.
To open the menu, press c on your computer or use the navigation controls in the mobile/web app.
The menu enables you to:
On your computer, use the mouse or arrow keys, enter, and escape to navigate. The mouse's thumb buttons go back and forward a page — back is the same as escape, and forward has no on-screen button, so right-click Back to see the page history and jump anywhere in it (at the top of the stack, where there is no Back button, right-click Home instead). On your phone, use the arrow buttons, ok, back, and home to navigate.
The remote's other two buttons work as well. Menu (the hamburger) opens the context menu of whatever is selected — the same menu you get by right-clicking, holding Play, Queue, Mark Watched, Favorite and Download, so those actions are reachable without a keyboard. During playback it opens the player's settings menu instead. Search puts the cursor in the search box; it does nothing during playback. Your keyboard's menu key does the same as the remote's, and opening a movie or episode from a remote or the arrow keys lands on its Play button, so pressing ok twice plays it.
The local library browser can hold several users, letting more than one person share the same player without their servers, sessions, and remote-control state colliding. Jellyfin (and jellyfin-web) has no built-in fast user switching; because this client owns its own UI, it can.
A user is a local grouping of one or more server logins that connect together. Only one user is active at a time. Switching disconnects the active user's servers and connects the selected user's, then updates the server selector.
(default) user
(which you can rename). Use Add User to create more, then Switch to a user and add its servers
with the normal Add a server form (each user's servers are managed while that user is active). Any
server address already used by another user is offered under Previously added servers with Use and
Quick Connect shortcuts, so you don't retype URLs when provisioning a new account.hostname (Kids)), so two users logged into the same server don't fight over one server-side
session. The (default) user keeps the original device id, so its existing sessions and tokens are
untouched.The first time you close the browser window, you're asked whether closing should Minimize to Tray (keep the app running as a cast target) or Exit. Your choice is remembered and can be changed later via Close to Tray (keep running) in Settings → General → Window.
Users are stored in users.json in the config folder (next to cred.json). On first run with this
feature, your existing cred.json is migrated into the (default) user automatically.
Shader packs let you use advanced video shaders and video quality settings without the configuration they normally require. MPV Shim's default shader pack comes with FSRCNNX and Anime4K preconfigured. Try experimenting with video profiles! It may greatly improve your experience.
To use, navigate to the Video Playback Profiles option and select a profile.
Profiles leave your graphics API alone, so HDR output keeps working. If video breaks
when you load one, pick a different API under Settings → Video Enhancement → Graphics
API for Shaders (shader_pack_gpu_api); opengl is the most compatible.
For details on the shader settings, please see default-shader-pack. If you would like to customize the shader pack, there are details in the configuration section.
SVP integration allows you to easily configure SVP support, change profiles, and enable/disable SVP without having to exit the player. It is not enabled by default, please see the configuration instructions for instructions on how to enable it.
If your server has a tuner, a Live TV entry appears with your other libraries (and a row of shortcuts on the home screen, next to "On Now"). It has the same six screens the web client does:
You can record from a listing without opening it: right-click any programme, in the guide or in any row, for Record / Record Series.
Guide Settings (top right of the Guide) controls channel order, favourites-first, the Live/New/Premiere/HD indicators, colour-coded categories and which categories are shown. These are stored on your server in the same place the web client keeps them, so changing them here changes them there and vice versa. Unticking a category empties the cells it covers rather than removing them, so the guide stays readable as a grid — again, as the web client does it.
One deliberate difference from the web client: the guide shows one window at a time (two hours on a typical window, more if there is room) and the arrows page it, rather than being a 24-hour grid you scroll sideways. That suits a remote better, and it avoids the scroll-sync stutter a grid inside the player's window would have.
Books libraries now appear with your other libraries, and the two things Jellyfin calls a "book" are handled quite differently — because the server treats them quite differently.
Audiobooks are ordinary audio. They play, queue, download and report progress like any other track, and browse through the now-playing bar at the bottom of the window. A books library opens as its folder tree (the same default the web client uses), and a folder holding the chapters of one audiobook is drawn as an album would be: a cover, Play / Resume / Add to Queue / Download, and a numbered chapter list. Resume picks up at the first chapter you have not finished, at the point you left it.
A single-file audiobook (.m4b) is one long item with chapters inside it, so
the audio bar grows controls for one: chapter back/forward around the play
button, back 10s / forward 30s either side of it, chapter ticks on the
scrubber so you can see where you are in the book rather than in the file,
and a chapter list. They appear only for an audiobook that actually has
chapters, so music is unaffected — and the bar drops them again on a narrow
window, keeping whichever matter most for what is playing.
Books you have finished get a tick on the shelf and part-read ones a progress bar, and right-clicking a book's folder can mark it finished or unfinished. Once you have started a book there is no bare "Play" button: the page offers Resume and, separately, Play from Beginning — starting a ten-hour book over by accident overwrites hours of position as it plays.
Books are download-and-open. There is no reader in the player, and there
will not be one: Jellyfin stores books but does not serve them — there is
no endpoint that returns a page, an archive entry or a chapter of an ebook,
so the only thing any client can fetch is the whole file. Rather than build a
worse reader than the one you already have, a book's page offers Read,
which downloads it if it is not already on disk and then hands it to whatever
your desktop opens that format with. Download beside it fetches the copy
and stops there. epub, pdf and the comic formats all
work this way, and so do mobi/azw3 — which the web client cannot open at
all, because it has no reader for them and we do not need one.
Downloaded books and audiobooks appear as their own two sections in Settings → Downloads, with audiobooks grouped by book so a thirty-file rip is one row rather than thirty.
Reading progress is manual, and deliberately so. Your reader does not report back to Jellyfin — nothing could make it — so a PDF or comic gets a Progress… button with Pull and Push: pull to see where another device left off before you open the file, push to record the page you actually got to when you close it.
Ebooks get no such button, on purpose. Jellyfin stores an epub's place as a position in the ebook reader's internal index (the text chopped into ~1000-character runs, counted per chapter), not as a page or a percentage of the book. No reader shows you that number, so there is nothing you could type — and the scale is not the one "percent" suggests, because the denominator depends on how the book was split into chapters. The figure is still read from the server and shown on the page; only setting it is left out, until there is a unit worth asking you for.
One thing to know: downloading is a permission, and for books it is not optional. If your account does not have "Allow media downloads", Read will say so — for a film that permission only costs you offline viewing, but for a book it is the only way to get at the content at all.
Casting an item from another Jellyfin client shows it on your display before you play it, similar to Chromecast. This is on by default and needs no configuration — the item's page opens in the library browser, and you can drive it from there with the remote's arrow keys. Display mirroring doesn't interrupt playback.
headless)For a box that should only be a cast target — a TV in a shared space, say — set headless
to true in the config file. The player then shows a "Ready to cast" backdrop instead of the
library, and the library cannot be reached from the machine itself: no browsing, no search, no
settings, and no queue view. Casting, playback and the player controls all work normally,
including transport controls for music.
This is not a security feature. It stops someone plugging in a mouse and playing random things from your library, which is what it is for. It does not stop anyone with real access to the machine: the config file is editable, and the systray menu still reaches Settings and the log viewer. If you need the box genuinely locked down, use the operating system for that.
This program supports most of the keyboard shortcuts from MPV. The custom keyboard shortcuts are:
Here are the notable MPV keyboard shortcuts:
Most settings are editable in the app under Settings, so you rarely need to touch the config file. The full list of options, including the ones with no UI, is in docs/configuration.md.
The config file lives in a per-platform folder — the systray icon can open it for you, and the path is printed at startup on Linux and macOS. See the reference for the locations.
Various tips have been found that allow the media player to support special functionality, albeit with more configuration required.
Please note: Edits to the mpv.conf will not take effect until you restart the application. You can open the config directory by using the menu option in the system tray icon.
Option 1: Select fullscreen output screen through MPV. Determine which screen you would like MPV to show up on.
xrandr. The screen number is the number you want. If there is only one proceed to Option 2.Add the following to your mpv.conf in the config directory, replacing 0 with the number from the previous step:
fs=yes
fs-screen=0
Option 2: (Linux Only) If option 1 does not work, both of your monitors are likely configured as a single "screen".
Run xrandr. It should look something like this:
Screen 0: minimum 8 x 8, current 3520 x 1080, maximum 16384 x 16384
VGA-0 connected 1920x1080+0+0 (normal left inverted right x axis y axis) 521mm x 293mm
1920x1080 60.00*+
1680x1050 59.95
1440x900 59.89
1280x1024 75.02 60.02
1280x960 60.00
1280x800 59.81
1280x720 60.00
1152x864 75.00
1024x768 75.03 70.07 60.00
800x600 75.00 72.19 60.32 56.25
640x480 75.00 59.94
LVDS-0 connected 1600x900+1920+180 (normal left inverted right x axis y axis) 309mm x 174mm
1600x900 59.98*+
If you want MPV to open on VGA-0 for instance, add the following to your mpv.conf in the config directory:
fs=yes
geometry=1920x1080+0+0
Option 3: (Linux Only) If your window manager supports it, you can tell the window manager to always open on a specific screen.
Add the following to input.conf:
WHEEL_UP add volume 5
WHEEL_DOWN add volume -5
Set mpv_ext to true in the config. Add script=/path/to/mpris.so to mpv.conf.
Pass --config /path/to/folder to run another copy of the player.
Each config directory gets its own instance: the single-instance guard is a lock inside the config directory, so copies pointed at different folders coexist by design. Launching a second copy with the same config directory instead raises the window of the one already running, which is what makes the desktop launcher and the tray behave sensibly.
To shut one down, run jellyfin-mpv-shim stop (with the same --config folder, if you used
one). It reaches the instance owning that directory over the same channel a second launch uses
to raise the window, so it stops the right copy without hunting for a process id, and the app
runs its normal shutdown rather than being killed. It exits non-zero only if an instance is
holding the lock but not answering.
This is built in now — see Audio Output. Set audio_mode to hdmi or
optical in Settings and tick the formats your receiver accepts; there is no need to hand-edit
mpv.conf.
This section used to recommend an mpv.conf snippet setting audio-spdif and
af=lavcac3enc together. Don't do that. The two are mutually exclusive per track: the AC3
encoder is handed a compressed frame it cannot convert, the filter chain fails to build, and mpv
recovers by silently disabling the filter — so the encoder never runs and nothing tells you why.
audio_mode=optical handles this properly by choosing between passthrough and the encoder per
track, based on what the track actually is.
Run the jellyfin-mpv-shim program with LC_NUMERIC=C.
Build instructions, dev installation, packaging and translation are in docs/development.md.
You can install the software from flathub. The pip installation is less integrated but takes up less space if you're not already using flatpak.
If you are on Linux, you can install via pip. You'll need libmpv or mpv installed.
MPV 0.41 or newer is recommended. Older versions work, with two differences:
minimizing the library to the tray quits MPV rather than just dropping its
window (it comes back on the next play, or from the tray), and copy/paste in
text fields needs wl-clipboard on Wayland or xclip/xsel on X11, because
MPV had no X11 clipboard of its own before 0.41.
pip3 install --upgrade jellyfin-mpv-shim
This installs the main UI, tkinter is not needed anymore. By default the application does not run in the background on Linux unless you enable it in settings or install the optional systray support:
pip3 install 'jellyfin-mpv-shim[systray]'
# Requires gi and appindicator, e.g. on Debian:
sudo apt install python3-gi gir1.2-ayatanaappindicator3-0.1
You must use a virtualenv with --system-site-packages enabled for systray support.
With pipx, use pipx install --system-site-packages 'jellyfin-mpv-shim[systray]'.
Discord rich presence support (must be enabled in config) can be installed with:
sudo pip3 install jellyfin-mpv-shim[discord]
If your distribution ships an old MPV, building it from source gets you better codec support and the current renderer. Follow the instructions in mpv-build — it builds MPV together with matching FFmpeg, libass and libplacebo, and its README lists the build dependencies for your distribution.
libmpv is what this client loads, and mpv-build produces it by default (libmpv defaults to
true in MPV's own meson options), so no extra configuration is needed. Afterwards run
sudo ldconfig so the new library is picked up.
Note, if it has been a while since you have compiled mpv, modern copies of mpv-build don't
support --enable-libmpv-shared in the mpv_options file, you can clear the file as libmpv
is now enabled by default. Add -Dsdl2-gamepad=enabled to mpv_options if you want gamepad
support.
Some of what the client draws is baked into the picture with Pillow, and Pillow
does complex text layout only through Raqm. Raqm and HarfBuzz are compiled
into Pillow's wheels, but FriBiDi is loaded at runtime and is not bundled on
any platform — so on a system without libfribidi, Pillow quietly falls back
to a basic layout. Nothing errors, and an English library looks perfectly fine.
What it costs, if it happens to you:
You can ask Pillow directly:
python3 -c "from PIL import features; print(features.check('raqm'))"
True and there is nothing to do — which is the normal case, because most
desktops already have FriBiDi (Pango needs it). If it prints False:
libfribidi0 on Debian/Ubuntu, fribidi on Fedora and
Arch — and check again;--no-binary), Pillow
needs libraqm's development files at build time too: libraqm-dev on
Debian/Ubuntu, libraqm-devel on Fedora, libraqm on Arch. Install those,
then pip3 install --force-reinstall --no-binary=pillow pillow.The Flatpak and the Windows installer both ship what they need for this.
Currently on macOS only the external MPV backend seems to be working. I cannot test on macOS, so please report any issues you find.
To install the CLI version:
brew install python mpvbrew install pipxpipx ensurepathpipx install jellyfin-mpv-shimjellyfin-mpv-shim.If you'd like the menu bar icon as well:
brew install mpvbrew install pythonbrew install pipxpipx ensurepathpipx install 'jellyfin-mpv-shim[systray]'jellyfin-mpv-shim.Display mirroring is not tested on macOS, but may be installable with 'pipx install 'jellyfin-mpv-shim[mirror]'`.
macOS installs the same Pillow wheels as Linux, so the FriBiDi note under Linux Installation applies here too — worth checking if Arabic or Hebrew titles look wrong.
There is a prebuilt version for Windows in the releases section, so you only need this if you are working on the client itself.
These steps mirror .github/workflows/main.yml, which is what actually produces the releases.
If this section and the workflow ever disagree, the workflow is right — check it first.
Make sure Python and libmpv are the same architecture; mismatching them fails at runtime.
On an Arm machine that takes care: Windows runs x64 binaries under emulation, so an x64 Python
will install and build without complaining and only the finished client will be wrong. CI checks
this with tools/check_win_arch.py rather than trusting it — do the same locally if unsure.
git clone https://github.com/jellyfin/jellyfin-mpv-shim; cd jellyfin-mpv-shim.
git pull.winget install --id JRSoftware.InnoSetup -e -s winget.cmd and run pip install wheel then pip install .[all] pywin32.mpv-dev-* archive, not the player build.
mpv-dev-x86_64-v3-*.7z. The v3 builds need a CPU supporting x86-64-v3; for older
hardware use the plain mpv-dev-x86_64-*-git-*.7z (this is what the "legacy64" release is).mpv-dev-aarch64-*.7z.libmpv-2.dll into the jellyfin-mpv-shim folder, renaming it to
mpv-2.dll. The build scripts look for that name../gen_pkg.sh --get-pyinstaller
cd pyinstaller/bootloader && python ./waf distclean all && cd .. && pip install .
cd ..
.exe. It is not a dependency of the
application, and nobody running the client needs it.pip install pyinstaller also works, but ships a prebuilt bootloader that
antivirus products have a long history of flagging. Building it locally gives the
installer a bootloader that isn't already on every heuristic blocklist, which is why CI
does it this way.--target-arch=64bit-arm --check-c-compiler=msvc. waf infers the target
from PROCESSOR_ARCHITECTURE and picks a compiler by search order; either one guessing
wrong builds an x64 bootloader and files it under the ARM64 name instead of erroring../gen_pkg.sh --skip-build.
msgfmt, which Git Bash does not ship. Where it is absent
the script falls back to tools/msgfmt.py, which produces the same catalogs
(tests/test_msgfmt.py asserts that against GNU msgfmt on all 86 locales).build-win.bat from cmd (build-win-arm64.bat for ARM64, build-win-dbg.bat
for a console-attached debug build). There is no 32-bit build — see
docs/packaging.md section 1.
mpv-2.dll in the same place — just extract the archive for
the architecture you want. There is no separate mpv32 folder./DArm64 to Inno Setup, which restricts the installer to Arm
machines and puts it in the 64-bit Program Files rather than the WOW64 one.Overall
Maintained
Code review
Latest release
Releases this year
159 followers · starred Mar 2025
315 followers · starred Apr 2024
111 followers · starred Aug 2025
25 followers · starred Aug 2020
Jellyfin MPV Shim is a cross-platform client for Jellyfin. It can run in the background as a cast target or act as a fully-featured desktop client with offline sync support.
It has support for all your advanced media files without transcoding, as well as tons of features which set it apart from other multimedia clients:
To learn more, keep reading. This README explains everything, including configuration, tips & tricks, and development information.
If you are on Windows, simply download the binary. If you are using Linux, you can install via flathub or install via pip. If you are on macOS, see the macOS Installation section below.
To use the client, simply launch it and log into your Jellyfin server. You’ll need to enter the
URL to your server, for example http://server_ip:8096 or https://secure_domain. Make sure to
include the subdirectory and port number if applicable. You can then cast your media
from another Jellyfin application. You can also use quick connect, which is easier and faster.
The application runs with a notification icon by default on Windows and if installed on Linux. If you would like the application to run in the background without the notification icon, enable it in settings.
Note: Due to the huge number of questions and issues that have been submitted about URLs, I now tolerate
bare IP addresses and not specifying the port by default. If you want to connect to port 80 instead of
8096, you must add the :80 to the URL because :8096 is now the default.
Please note the following issues with controlling SyncPlay:
Music playback works, but gapless playback is not planned at this time.
The shader packs feature is sensitive to graphics hardware. If the application fails to launch, use
--reset-shaders command line argument to reset the shader configuration. If it's merely garbled graphics
you can press k to disable the last used shader config.
Most of these are also reachable from the player UI's settings (gear) menu, which is usually easier. This menu is the older text-based one, and it still covers a few things the player UI does not.
To open the menu, press c on your computer or use the navigation controls in the mobile/web app.
The menu enables you to:
On your computer, use the mouse or arrow keys, enter, and escape to navigate. The mouse's thumb buttons go back and forward a page — back is the same as escape, and forward has no on-screen button, so right-click Back to see the page history and jump anywhere in it (at the top of the stack, where there is no Back button, right-click Home instead). On your phone, use the arrow buttons, ok, back, and home to navigate.
The remote's other two buttons work as well. Menu (the hamburger) opens the context menu of whatever is selected — the same menu you get by right-clicking, holding Play, Queue, Mark Watched, Favorite and Download, so those actions are reachable without a keyboard. During playback it opens the player's settings menu instead. Search puts the cursor in the search box; it does nothing during playback. Your keyboard's menu key does the same as the remote's, and opening a movie or episode from a remote or the arrow keys lands on its Play button, so pressing ok twice plays it.
The local library browser can hold several users, letting more than one person share the same player without their servers, sessions, and remote-control state colliding. Jellyfin (and jellyfin-web) has no built-in fast user switching; because this client owns its own UI, it can.
A user is a local grouping of one or more server logins that connect together. Only one user is active at a time. Switching disconnects the active user's servers and connects the selected user's, then updates the server selector.
(default) user
(which you can rename). Use Add User to create more, then Switch to a user and add its servers
with the normal Add a server form (each user's servers are managed while that user is active). Any
server address already used by another user is offered under Previously added servers with Use and
Quick Connect shortcuts, so you don't retype URLs when provisioning a new account.hostname (Kids)), so two users logged into the same server don't fight over one server-side
session. The (default) user keeps the original device id, so its existing sessions and tokens are
untouched.The first time you close the browser window, you're asked whether closing should Minimize to Tray (keep the app running as a cast target) or Exit. Your choice is remembered and can be changed later via Close to Tray (keep running) in Settings → General → Window.
Users are stored in users.json in the config folder (next to cred.json). On first run with this
feature, your existing cred.json is migrated into the (default) user automatically.
Shader packs let you use advanced video shaders and video quality settings without the configuration they normally require. MPV Shim's default shader pack comes with FSRCNNX and Anime4K preconfigured. Try experimenting with video profiles! It may greatly improve your experience.
To use, navigate to the Video Playback Profiles option and select a profile.
Profiles leave your graphics API alone, so HDR output keeps working. If video breaks
when you load one, pick a different API under Settings → Video Enhancement → Graphics
API for Shaders (shader_pack_gpu_api); opengl is the most compatible.
For details on the shader settings, please see default-shader-pack. If you would like to customize the shader pack, there are details in the configuration section.
SVP integration allows you to easily configure SVP support, change profiles, and enable/disable SVP without having to exit the player. It is not enabled by default, please see the configuration instructions for instructions on how to enable it.
If your server has a tuner, a Live TV entry appears with your other libraries (and a row of shortcuts on the home screen, next to "On Now"). It has the same six screens the web client does:
You can record from a listing without opening it: right-click any programme, in the guide or in any row, for Record / Record Series.
Guide Settings (top right of the Guide) controls channel order, favourites-first, the Live/New/Premiere/HD indicators, colour-coded categories and which categories are shown. These are stored on your server in the same place the web client keeps them, so changing them here changes them there and vice versa. Unticking a category empties the cells it covers rather than removing them, so the guide stays readable as a grid — again, as the web client does it.
One deliberate difference from the web client: the guide shows one window at a time (two hours on a typical window, more if there is room) and the arrows page it, rather than being a 24-hour grid you scroll sideways. That suits a remote better, and it avoids the scroll-sync stutter a grid inside the player's window would have.
Books libraries now appear with your other libraries, and the two things Jellyfin calls a "book" are handled quite differently — because the server treats them quite differently.
Audiobooks are ordinary audio. They play, queue, download and report progress like any other track, and browse through the now-playing bar at the bottom of the window. A books library opens as its folder tree (the same default the web client uses), and a folder holding the chapters of one audiobook is drawn as an album would be: a cover, Play / Resume / Add to Queue / Download, and a numbered chapter list. Resume picks up at the first chapter you have not finished, at the point you left it.
A single-file audiobook (.m4b) is one long item with chapters inside it, so
the audio bar grows controls for one: chapter back/forward around the play
button, back 10s / forward 30s either side of it, chapter ticks on the
scrubber so you can see where you are in the book rather than in the file,
and a chapter list. They appear only for an audiobook that actually has
chapters, so music is unaffected — and the bar drops them again on a narrow
window, keeping whichever matter most for what is playing.
Books you have finished get a tick on the shelf and part-read ones a progress bar, and right-clicking a book's folder can mark it finished or unfinished. Once you have started a book there is no bare "Play" button: the page offers Resume and, separately, Play from Beginning — starting a ten-hour book over by accident overwrites hours of position as it plays.
Books are download-and-open. There is no reader in the player, and there
will not be one: Jellyfin stores books but does not serve them — there is
no endpoint that returns a page, an archive entry or a chapter of an ebook,
so the only thing any client can fetch is the whole file. Rather than build a
worse reader than the one you already have, a book's page offers Read,
which downloads it if it is not already on disk and then hands it to whatever
your desktop opens that format with. Download beside it fetches the copy
and stops there. epub, pdf and the comic formats all
work this way, and so do mobi/azw3 — which the web client cannot open at
all, because it has no reader for them and we do not need one.
Downloaded books and audiobooks appear as their own two sections in Settings → Downloads, with audiobooks grouped by book so a thirty-file rip is one row rather than thirty.
Reading progress is manual, and deliberately so. Your reader does not report back to Jellyfin — nothing could make it — so a PDF or comic gets a Progress… button with Pull and Push: pull to see where another device left off before you open the file, push to record the page you actually got to when you close it.
Ebooks get no such button, on purpose. Jellyfin stores an epub's place as a position in the ebook reader's internal index (the text chopped into ~1000-character runs, counted per chapter), not as a page or a percentage of the book. No reader shows you that number, so there is nothing you could type — and the scale is not the one "percent" suggests, because the denominator depends on how the book was split into chapters. The figure is still read from the server and shown on the page; only setting it is left out, until there is a unit worth asking you for.
One thing to know: downloading is a permission, and for books it is not optional. If your account does not have "Allow media downloads", Read will say so — for a film that permission only costs you offline viewing, but for a book it is the only way to get at the content at all.
Casting an item from another Jellyfin client shows it on your display before you play it, similar to Chromecast. This is on by default and needs no configuration — the item's page opens in the library browser, and you can drive it from there with the remote's arrow keys. Display mirroring doesn't interrupt playback.
headless)For a box that should only be a cast target — a TV in a shared space, say — set headless
to true in the config file. The player then shows a "Ready to cast" backdrop instead of the
library, and the library cannot be reached from the machine itself: no browsing, no search, no
settings, and no queue view. Casting, playback and the player controls all work normally,
including transport controls for music.
This is not a security feature. It stops someone plugging in a mouse and playing random things from your library, which is what it is for. It does not stop anyone with real access to the machine: the config file is editable, and the systray menu still reaches Settings and the log viewer. If you need the box genuinely locked down, use the operating system for that.
This program supports most of the keyboard shortcuts from MPV. The custom keyboard shortcuts are:
Here are the notable MPV keyboard shortcuts:
Most settings are editable in the app under Settings, so you rarely need to touch the config file. The full list of options, including the ones with no UI, is in docs/configuration.md.
The config file lives in a per-platform folder — the systray icon can open it for you, and the path is printed at startup on Linux and macOS. See the reference for the locations.
Various tips have been found that allow the media player to support special functionality, albeit with more configuration required.
Please note: Edits to the mpv.conf will not take effect until you restart the application. You can open the config directory by using the menu option in the system tray icon.
Option 1: Select fullscreen output screen through MPV. Determine which screen you would like MPV to show up on.
xrandr. The screen number is the number you want. If there is only one proceed to Option 2.Add the following to your mpv.conf in the config directory, replacing 0 with the number from the previous step:
fs=yes
fs-screen=0
Option 2: (Linux Only) If option 1 does not work, both of your monitors are likely configured as a single "screen".
Run xrandr. It should look something like this:
Screen 0: minimum 8 x 8, current 3520 x 1080, maximum 16384 x 16384
VGA-0 connected 1920x1080+0+0 (normal left inverted right x axis y axis) 521mm x 293mm
1920x1080 60.00*+
1680x1050 59.95
1440x900 59.89
1280x1024 75.02 60.02
1280x960 60.00
1280x800 59.81
1280x720 60.00
1152x864 75.00
1024x768 75.03 70.07 60.00
800x600 75.00 72.19 60.32 56.25
640x480 75.00 59.94
LVDS-0 connected 1600x900+1920+180 (normal left inverted right x axis y axis) 309mm x 174mm
1600x900 59.98*+
If you want MPV to open on VGA-0 for instance, add the following to your mpv.conf in the config directory:
fs=yes
geometry=1920x1080+0+0
Option 3: (Linux Only) If your window manager supports it, you can tell the window manager to always open on a specific screen.
Add the following to input.conf:
WHEEL_UP add volume 5
WHEEL_DOWN add volume -5
Set mpv_ext to true in the config. Add script=/path/to/mpris.so to mpv.conf.
Pass --config /path/to/folder to run another copy of the player.
Each config directory gets its own instance: the single-instance guard is a lock inside the config directory, so copies pointed at different folders coexist by design. Launching a second copy with the same config directory instead raises the window of the one already running, which is what makes the desktop launcher and the tray behave sensibly.
To shut one down, run jellyfin-mpv-shim stop (with the same --config folder, if you used
one). It reaches the instance owning that directory over the same channel a second launch uses
to raise the window, so it stops the right copy without hunting for a process id, and the app
runs its normal shutdown rather than being killed. It exits non-zero only if an instance is
holding the lock but not answering.
This is built in now — see Audio Output. Set audio_mode to hdmi or
optical in Settings and tick the formats your receiver accepts; there is no need to hand-edit
mpv.conf.
This section used to recommend an mpv.conf snippet setting audio-spdif and
af=lavcac3enc together. Don't do that. The two are mutually exclusive per track: the AC3
encoder is handed a compressed frame it cannot convert, the filter chain fails to build, and mpv
recovers by silently disabling the filter — so the encoder never runs and nothing tells you why.
audio_mode=optical handles this properly by choosing between passthrough and the encoder per
track, based on what the track actually is.
Run the jellyfin-mpv-shim program with LC_NUMERIC=C.
Build instructions, dev installation, packaging and translation are in docs/development.md.
You can install the software from flathub. The pip installation is less integrated but takes up less space if you're not already using flatpak.
If you are on Linux, you can install via pip. You'll need libmpv or mpv installed.
MPV 0.41 or newer is recommended. Older versions work, with two differences:
minimizing the library to the tray quits MPV rather than just dropping its
window (it comes back on the next play, or from the tray), and copy/paste in
text fields needs wl-clipboard on Wayland or xclip/xsel on X11, because
MPV had no X11 clipboard of its own before 0.41.
pip3 install --upgrade jellyfin-mpv-shim
This installs the main UI, tkinter is not needed anymore. By default the application does not run in the background on Linux unless you enable it in settings or install the optional systray support:
pip3 install 'jellyfin-mpv-shim[systray]'
# Requires gi and appindicator, e.g. on Debian:
sudo apt install python3-gi gir1.2-ayatanaappindicator3-0.1
You must use a virtualenv with --system-site-packages enabled for systray support.
With pipx, use pipx install --system-site-packages 'jellyfin-mpv-shim[systray]'.
Discord rich presence support (must be enabled in config) can be installed with:
sudo pip3 install jellyfin-mpv-shim[discord]
If your distribution ships an old MPV, building it from source gets you better codec support and the current renderer. Follow the instructions in mpv-build — it builds MPV together with matching FFmpeg, libass and libplacebo, and its README lists the build dependencies for your distribution.
libmpv is what this client loads, and mpv-build produces it by default (libmpv defaults to
true in MPV's own meson options), so no extra configuration is needed. Afterwards run
sudo ldconfig so the new library is picked up.
Note, if it has been a while since you have compiled mpv, modern copies of mpv-build don't
support --enable-libmpv-shared in the mpv_options file, you can clear the file as libmpv
is now enabled by default. Add -Dsdl2-gamepad=enabled to mpv_options if you want gamepad
support.
Some of what the client draws is baked into the picture with Pillow, and Pillow
does complex text layout only through Raqm. Raqm and HarfBuzz are compiled
into Pillow's wheels, but FriBiDi is loaded at runtime and is not bundled on
any platform — so on a system without libfribidi, Pillow quietly falls back
to a basic layout. Nothing errors, and an English library looks perfectly fine.
What it costs, if it happens to you:
You can ask Pillow directly:
python3 -c "from PIL import features; print(features.check('raqm'))"
True and there is nothing to do — which is the normal case, because most
desktops already have FriBiDi (Pango needs it). If it prints False:
libfribidi0 on Debian/Ubuntu, fribidi on Fedora and
Arch — and check again;--no-binary), Pillow
needs libraqm's development files at build time too: libraqm-dev on
Debian/Ubuntu, libraqm-devel on Fedora, libraqm on Arch. Install those,
then pip3 install --force-reinstall --no-binary=pillow pillow.The Flatpak and the Windows installer both ship what they need for this.
Currently on macOS only the external MPV backend seems to be working. I cannot test on macOS, so please report any issues you find.
To install the CLI version:
brew install python mpvbrew install pipxpipx ensurepathpipx install jellyfin-mpv-shimjellyfin-mpv-shim.If you'd like the menu bar icon as well:
brew install mpvbrew install pythonbrew install pipxpipx ensurepathpipx install 'jellyfin-mpv-shim[systray]'jellyfin-mpv-shim.Display mirroring is not tested on macOS, but may be installable with 'pipx install 'jellyfin-mpv-shim[mirror]'`.
macOS installs the same Pillow wheels as Linux, so the FriBiDi note under Linux Installation applies here too — worth checking if Arabic or Hebrew titles look wrong.
There is a prebuilt version for Windows in the releases section, so you only need this if you are working on the client itself.
These steps mirror .github/workflows/main.yml, which is what actually produces the releases.
If this section and the workflow ever disagree, the workflow is right — check it first.
Make sure Python and libmpv are the same architecture; mismatching them fails at runtime.
On an Arm machine that takes care: Windows runs x64 binaries under emulation, so an x64 Python
will install and build without complaining and only the finished client will be wrong. CI checks
this with tools/check_win_arch.py rather than trusting it — do the same locally if unsure.
git clone https://github.com/jellyfin/jellyfin-mpv-shim; cd jellyfin-mpv-shim.
git pull.winget install --id JRSoftware.InnoSetup -e -s winget.cmd and run pip install wheel then pip install .[all] pywin32.mpv-dev-* archive, not the player build.
mpv-dev-x86_64-v3-*.7z. The v3 builds need a CPU supporting x86-64-v3; for older
hardware use the plain mpv-dev-x86_64-*-git-*.7z (this is what the "legacy64" release is).mpv-dev-aarch64-*.7z.libmpv-2.dll into the jellyfin-mpv-shim folder, renaming it to
mpv-2.dll. The build scripts look for that name../gen_pkg.sh --get-pyinstaller
cd pyinstaller/bootloader && python ./waf distclean all && cd .. && pip install .
cd ..
.exe. It is not a dependency of the
application, and nobody running the client needs it.pip install pyinstaller also works, but ships a prebuilt bootloader that
antivirus products have a long history of flagging. Building it locally gives the
installer a bootloader that isn't already on every heuristic blocklist, which is why CI
does it this way.--target-arch=64bit-arm --check-c-compiler=msvc. waf infers the target
from PROCESSOR_ARCHITECTURE and picks a compiler by search order; either one guessing
wrong builds an x64 bootloader and files it under the ARM64 name instead of erroring../gen_pkg.sh --skip-build.
msgfmt, which Git Bash does not ship. Where it is absent
the script falls back to tools/msgfmt.py, which produces the same catalogs
(tests/test_msgfmt.py asserts that against GNU msgfmt on all 86 locales).build-win.bat from cmd (build-win-arm64.bat for ARM64, build-win-dbg.bat
for a console-attached debug build). There is no 32-bit build — see
docs/packaging.md section 1.
mpv-2.dll in the same place — just extract the archive for
the architecture you want. There is no separate mpv32 folder./DArm64 to Inno Setup, which restricts the installer to Arm
machines and puts it in the 64-bit Program Files rather than the WOW64 one.Overall
Maintained
Code review
Latest release
Releases this year
159 followers · starred Mar 2025
315 followers · starred Apr 2024
111 followers · starred Aug 2025
25 followers · starred Aug 2020