Jellyfin 之前一直跑在笔记本的 Docker 虚拟机里,全靠 CPU 软解软编。平时电视直接播放(Direct Play)没问题,可一旦遇到电视不支持的格式,比如 HEVC 10bit 的片源,或者要把 ASS 特效字幕烧进画面,就得实时转码,两个虚拟 CPU 瞬间满载,画面一卡一卡的。
最直接的办法是让核显来干活。但在笔记本上给 Proxmox 虚拟机直通核显很折腾:之前那篇说过,整块设备直通只能给虚拟机,而笔记本的核显直通坑又特别多。权衡之后,我买了一台 N100 的小主机(192.168.10.30),装 Debian 13,直接在宿主机上跑 Docker,媒体相关的服务都搬了过去。它的核显支持 HEVC 10bit 和 AV1 的硬件解码,功耗又低,正适合干这个。
宿主机准备
Jellyfin 官方的 Docker 镜像里已经自带了打过补丁的 ffmpeg(jellyfin-ffmpeg)和核显的用户态媒体驱动(iHD),宿主机只要内核驱动能正常识别核显就行。Debian 13 的内核足够新,装完直接就能用,再确认一下核显需要的固件包装上了(Debian 上在 non-free-firmware 组件里)。
ls -l /dev/dri
# crw-rw---- 1 root video 226, 1 ... card1
# crw-rw---- 1 root render 226, 128 ... renderD128
getent group render # 看 render 组的 GID,每台机器可能不一样
# render:x:992:
dmesg | grep -iE 'guc|huc' # 看 GuC/HuC 固件有没有加载成功
apt install intel-gpu-tools # 带 intel_gpu_top,后面验证用
renderD128 就是转码要用的设备节点,属于 render 组。记下 render 组的 GID,下面要用。
compose 文件
还是按老规矩放在 /srv/compose/jellyfin:
services:
jellyfin:
image: jellyfin/jellyfin:10.11.0
container_name: jellyfin
restart: unless-stopped
user: "1000:1000"
group_add:
- "992" # 宿主机 render 组的 GID,要写数字
devices:
- /dev/dri:/dev/dri
volumes:
- ./config:/config
- ./cache:/cache
- /mnt/media:/media:ro
ports:
- "8096:8096"
environment:
TZ: Asia/Shanghai
关键是两处:devices 把 /dev/dri 映射进容器;group_add 让容器里的进程加入宿主机 render 组的 GID,这样才有权限打开 renderD128。媒体文件在 NAS 上,通过 NFS 只读挂到 /mnt/media。
启动后,先在容器里验证驱动:
docker exec -it jellyfin /usr/lib/jellyfin-ffmpeg/vainfo --display drm --device /dev/dri/renderD128
# 正常的话能看到 Intel iHD driver,以及一长串 VAProfile 和 VAEntrypoint
Jellyfin 里的设置
在“控制台 → 播放 → 转码”里:
- 硬件加速选 Intel QuickSync (QSV),设备保持默认的 /dev/dri/renderD128;
- 硬件解码把 H264、HEVC、HEVC 10bit、VP9、AV1 等都勾上,这一代核显都支持;
- 勾上启用硬件编码;
- 片源有 HDR 的话,打开色调映射,否则 HDR 片子转码出来颜色发灰;
- 底下的 Low-Power 编码选项要看核显代际,有些核显需要额外加载 HuC 固件才能用。我这台 dmesg 里能看到 HuC 加载成功,就勾上了。
至于 QSV 和 VAAPI 怎么选:在 Linux 上,QSV 本身就构建在 VA-API 之上,两者都需要 /dev/dri。Jellyfin 文档的建议是较新的核显用 QSV,比较老的才用 VAAPI。我两个都试了,都能正常转码,最后按文档选了 QSV。
设置好后,找一部 4K HEVC 10bit 的片子,在播放器里手动把码率调低来强制转码,同时在宿主机上开着 intel_gpu_top,能看到 Video 那一栏的占用跳起来,就说明硬件转码生效了。播放信息面板里也能看到转码的原因和用的是哪种编解码器。
踩过的坑
1. group_add 写了组名。 我一开始写的是组名 render,结果一播放就报错,日志里是打不开 renderD128 的权限错误。原因是组名是在容器里解析的,容器里的 render 组 GID 和宿主机不一定一样。改成宿主机上查到的数字 GID 就好了。
2. 媒体目录只读导致刮削报错。 媒体目录是只读挂进去的,结果 Jellyfin 刮削时一直报写入失败。在媒体库设置里把“将媒体图像保存到媒体文件夹”关掉,元数据保存器里的 NFO 也不勾,元数据和图片就都存到 /config 里了。
3. 电视找不到服务器。 电视在 IoT 网段里,而 Jellyfin 客户端的自动发现靠的是局域网广播,跨不了网段;手动输入地址也连不上,因为防火墙不让 IoT 访问内网。最后在路由器的 forward 链里、那条“丢弃私有地址”的规则前面加了一条,只放行电视到这台机器的 8096 端口:
iifname $IOT ip saddr 192.168.20.50 ip daddr 192.168.10.30 tcp dport 8096 accept
电视的 IP 在 dnsmasq 里按 MAC 地址做了固定分配,保证不会变。
4. 字幕也会触发转码。 即使视频格式电视能直接播放,只要选了需要烧录的图形字幕或特效字幕,也会整段转码。这部分虽然也能用上硬件,但能用外挂文本字幕的就尽量用文本字幕。
小结
换成核显转码之后,同一部 4K HDR 片子转成 1080p,CPU 占用从满载降到了二三十,整机功耗也就十几瓦,风扇基本听不到。N100 这类小主机拿来跑家庭影音确实合适,唯一的门槛就是 /dev/dri 的映射和 render 组的权限,弄明白之后其实就几行配置。