Jellyfin on QNAP: How I Enabled Hardware Acceleration on the Rockchip RK3588
From identifying devices in /dev to testing FFmpeg. My Jellyfin setup on a QNAP TS-AI642, including RKMPP, RGA, and the renderD129 trap.
My Docker Compose file for Jellyfin has six entries under devices. Copying them takes a few seconds. Understanding what they expose to the container, and whether each one helps play a video, takes a little more work.
Inspecting my running QNAP turned up a useful example: /dev/dri/renderD129 belongs to the NPU, the chip used for AI workloads. A device name containing renderD does not automatically identify hardware for video transcoding.
This is a walkthrough of my TS-AI642 setup: identifying the hardware, inspecting its devices, passing them into a container, and checking that acceleration works. While preparing this article, I inspected the NAS and ran a short H.264 test. That gives me a working example to share, with clear limits on what I verified.
Identify the NAS before choosing an acceleration method
QNAP makes devices with different architectures. A guide written for an Intel model may lead you to settings that do not apply to an ARM-based NAS.
My setup, checked on September 17, 2026, is a QNAP TS-AI642 with a Rockchip RK3588, QTS 5.2.10, and kernel 5.10.60-qnap. Jellyfin runs in the official jellyfin/jellyfin:12.1 image, and its bundled FFmpeg reports version 8.1.2-Jellyfin. These are the versions in this installation, rather than a recommendation to use them on every NAS.
After connecting over SSH, start by identifying the system:
uname -muname -rcat /proc/device-tree/modeltr '\0' '\n' < /proc/device-tree/compatible
On my device, the output includes aarch64, QNAP TS-AI642, and rockchip,rk3588. These device tree files are available on this ARM platform; they may not exist on an x86 QNAP. You can also check the model and processor in QTS.
The RK3588 contains several specialized processing units. The VPU decodes and encodes video, and Jellyfin accesses it through RKMPP. RGA handles operations such as image scaling. The Mali GPU provides the separate OpenCL path used for hardware HDR tone mapping. Jellyfin's Rockchip documentation explains this division of work.
For this setup, I select Rockchip MPP (RKMPP). Finding a /dev/dri directory alone is not a reason to select VA-API or Intel Quick Sync.
Work out what belongs in devices
I run the following commands over SSH on the QNAP. The path on the left side of a devices mapping belongs to the host running Docker. With a remote Docker context, that means the NAS, even when I type docker compose on my laptop.
First, list the available devices:
ls -ln /dev/drils -ln /dev/mpp_service /dev/rga /dev/mali0ls -ln /dev/dma_heap
All of these paths exist in my installation, but they serve different purposes:
/dev/mpp_serviceexposes the MPP interface used for video processing./dev/rgaprovides access to RGA, including hardware scaling./dev/dma_heap/*exposes DMA buffer allocation interfaces./dev/mali0is the Mali GPU device, relevant to the OpenCL path./dev/dri/card*and/dev/dri/renderD*are DRM nodes; you still need to identify which devices they belong to.
Names and numbering can differ between systems. I avoid adding nonexistent devices from someone else's example. If mpp_service or rga is missing, I go back to the NAS model and the drivers provided by QTS. An entry in a YAML file cannot make Docker supply missing hardware support.
Find out what renderD128 and renderD129 actually are
You can inspect the relationship between a device and its driver through sysfs:
for node in /sys/class/drm/card[0-9] /sys/class/drm/renderD*; do[ -e "$node" ] || continueecho "$node"readlink -f "$node/device"readlink -f "$node/device/driver"done
On my TS-AI642, the result maps the nodes as follows:
card0, renderD128 -> display-subsystem -> rockchip-drmcard1, renderD129 -> fdab0000.npu -> RKNPU
The second pair belongs to the NPU. I do not treat it as a second video encoding device. RKMPP uses the MPP interface; picking a renderD number does not replace that configuration. The Linux kernel's DRM documentation explains the general roles of card* and renderD* nodes.
The Docker Compose configuration I use
Here is the relevant part of my configuration. I have left out the public server URL setting and additional media libraries because they do not control hardware acceleration:
name: jellyfinservices:jellyfin:image: jellyfin/jellyfin:12.1container_name: jellyfinrestart: unless-stoppeduser: "0:0"privileged: trueports:- "8096:8096"devices:- /dev/mpp_service:/dev/mpp_service- /dev/rga:/dev/rga- /dev/dri/card0:/dev/dri/card0- /dev/dri/card1:/dev/dri/card1- /dev/dri/renderD128:/dev/dri/renderD128- /dev/dri/renderD129:/dev/dri/renderD129environment:TZ: Europe/Warsawvolumes:- /share/Cinema/Movies:/movies:rw- jellyfin_cache:/cache- jellyfin_config:/configvolumes:jellyfin_cache:external: truename: jellyfin_cachejellyfin_config:external: truename: jellyfin_config
This example documents the running deployment. Its device list also includes the NPU mentioned earlier. I have not tested the minimum set of mappings and am not claiming that all six are necessary.
The setting with the broadest effect here is privileged: true. Docker gives a privileged container access to all host devices and extensive permissions. That is why /dev/dma_heap and /dev/mali0 are visible inside my container even though they are absent from the devices list. In this mode, the list does not restrict device access. The Docker documentation describes this behavior.
The user: "0:0" setting runs the process as root. That matters because the MPP, RGA, and DRM devices on my NAS have crw------- permissions and are owned by 0:0. Adding an ordinary user to the video group would not grant access to a device whose group has no read or write permissions.
Reproducing this setup means accepting that broad level of access. Running without privileged requires a separate check of device mappings, permissions, and container restrictions. Removing one line and assuming the remaining mappings cover the entire processing path would leave that work undone.
Replace /share/Cinema/Movies with your own media directory. Volumes marked external: true must already exist. For a new installation, create them with docker volume create jellyfin_cache and docker volume create jellyfin_config. For an existing server, keep its configuration volume so that you do not accidentally start a fresh installation.
Once the file is ready, validate its syntax before applying it:
docker compose -f docker-compose.yaml config --quietdocker compose -f docker-compose.yaml up -d jellyfin
The second command can recreate the container and interrupt playback. When working from my laptop, I specify the remote context explicitly, for example: docker --context qnap compose -f docker-compose.yaml up -d jellyfin. The name qnap must match a context you have already configured.
Configure Jellyfin and inspect the container
In the Jellyfin dashboard, I open the playback and transcoding settings, select RKMPP, and enable hardware encoding. These fields in my /config/config/encoding.xml confirm the saved settings:
<HardwareAccelerationType>rkmpp</HardwareAccelerationType><EnableHardwareEncoding>true</EnableHardwareEncoding>
Use the dashboard to save changes. The XML is useful for checking the configuration; there is no need to edit the file manually while the server is running.
I would not enable every codec merely because it appears in the form. On my installation, the RKMPP decoder list does not include VC-1. The encoder list includes H.264 and HEVC, but not AV1. AV1 decoding and AV1 encoding are separate capabilities. Enabling an encoding option does not add a hardware encoder.
Next, check what the container can actually see:
docker exec jellyfin iddocker exec jellyfin ls -ln /dev/dri /dev/dma_heap \/dev/mpp_service /dev/rga /dev/mali0docker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffmpeg \-hide_banner -decoders 2>/dev/null | grep rkmppdocker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffmpeg \-hide_banner -encoders 2>/dev/null | grep rkmppdocker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffmpeg \-hide_banner -filters 2>/dev/null | grep rkrga
My results include h264_rkmpp, hevc_rkmpp, and scale_rkrga. I use the FFmpeg bundled with Jellyfin at /usr/lib/jellyfin-ffmpeg/ffmpeg. These lists show what this FFmpeg build supports. Running an operation is what checks whether it can work with the devices and drivers.
Run a test that actually uses the hardware
You do not need a film from your library for a basic encoder test. FFmpeg can generate a test pattern, encode 60 frames through RKMPP, and discard the output:
docker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffmpeg \-hide_banner \-f lavfi -i testsrc2=size=1280x720:rate=30 \-frames:v 60 -vf format=nv12 \-c:v h264_rkmpp -b:v 2M -f null -
I expect the command to finish without errors, identify h264_rkmpp as the encoder, and report 60 processed frames. This checks hardware encoding. Because the input is generated synthetically, it does not test a decoder.
When verifying this installation, I also ran a second stage: I piped the generated H.264 stream into the h264_rkmpp decoder, scaled it from 1280×720 to 640×360 with scale_rkrga, and encoded it again with h264_rkmpp. The complete pipeline exited with code 0 and processed 60 frames.
That confirms working H.264 decoding, scaling, and encoding inside this container. Such a short sample cannot establish how many concurrent 4K streams the NAS can handle or how much CPU usage falls. I did not measure either of those here.
Follow up with playback through a client
The FFmpeg test should be followed by playback through Jellyfin. Choose a video and set a lower resolution or bitrate in the client to trigger video transcoding. Check the playback method in the server dashboard, then open the corresponding FFmpeg log in the administration logs.
For the Rockchip hardware path, look for arguments such as -hwaccel rkmpp, an encoder such as h264_rkmpp or hevc_rkmpp, and, when scaling is required, a filter such as scale_rkrga. Read beyond the command line: starting a process does not prove it completed without errors.
Direct Play delivers the media without transcoding. Remux changes the media container, while Direct Stream can process audio without re-encoding the video. An idle VPU is expected in those cases. Jellyfin's transcoding documentation explains these playback methods.
For this article, I verified the synthetic FFmpeg pipeline. I did not run a new playback session through a client, so this final step remains a check to perform with your own combination of media, client, and settings.
Treat HDR as a separate test
Tone mapping is enabled in my Jellyfin settings. However, the attempt to initialize RKMPP together with OpenCL during verification stalled while waiting in the kernel. That test did not confirm working HDR-to-SDR conversion.
I would therefore start with SDR material and H.264. Once that works, I would move on to HDR and OpenCL diagnostics. Working MPP and RGA do not establish that the Mali library and the QNAP driver work together correctly.
If ordinary transcoding works and the problem appears only with HDR, narrow the investigation to tone mapping, OpenCL, and driver compatibility. A log from that specific playback session will be more useful than adding more entries to devices.