【投稿】扭曲的产物——为在 WSL Docker 中运行的 Jellyfin 配置 AMD APU 硬件加速

0
Want create site? Find Free WordPress Themes and plugins.

背景

作为一名日本动画观众,在这个时代拥有一个自己的 Jellyfin 流媒体平台似乎最好的选择。之前我将整套平台部署在了 Linux 台式机的 Docker 中,供自己和实验室其他一些同学使用。不过,下周一就要放假了,很快就要面临寒假断电的难题。之前几年我的解决方法一直都是:不管,中断服务,临时在笔记本上手动下载动画并观看。这自然不是一个很好的做法。

今年,顺带于回家编程的需求,购买了一台 AMD Ryzen™ 7 7840H(仅限中国大陆) 的迷你主机,那么自然就可以将整套服务迁移过来,回家时直接包里带走。由于前一段时间听说 WSL2 有了显著的更新1,可以与 Windows 系统使用相同的网络、支持内存和硬盘空间的自动回收,又由于这次购买了两条 24 GiB 的内存,内存比较充裕,所以决定了安装 Windows 23H2,并将 WSL2 作为日常编程的环境。

初步尝试

以上是背景。我完成系统安装,各种配置,将整个媒体库复制过来等操作之后,在启动 Jellyfin 的服务时便遇到了障碍:无法使用硬件加速。这意味着我无法进行转码或是进行服务端的字幕烧录。WSLg 对 NVIDIA 的 GPU 已经有了较好的支持,但对 AMD 的支持则较差,只能用图形加速进行窗口渲染,而无法进行 GPGPU 的计算。

在 2023 年 2 月,微软为 WSL2 加入了基于 D3D12 的 VA-API 支持2,也给出了在 WSL2 Docker 中使用 VAAPI 进行图形加速的方法3。但是,我一番操作之后却未能成功。实际上,我在 WSL 中执行命令

LIBVA_DRIVER_NAME vainfo --display drm --device /dev/dri/card0

得到的结果并没有可用于视频解码和编码的 Profile:

Trying display: drm  
Xlib:  extension "DRI2" missing on display ":0".  
vainfo: VA-API version: 1.20 (libva 2.20.1)  
vainfo: Driver version: Mesa Gallium driver 23.3.2-arch1.2 for D3D12 (AMD Radeon 780M Graphics)  
vainfo: Supported profile and entrypoints  
     VAProfileNone                   : VAEntrypointVideoProc

不知道是不是微软或者 AMD 没有写驱动,如果你足够幸运,在这里看到了更多的 Profile,下面的就都不用看了。不过奇怪的是,微软在后续另一篇文章4中提到,他们把给 WSL 用的 VA-API 驱动移植到了 Windows 上,现在 Windows 上也可以用 VA-API 了。而 Windows 上的 vainfo 命令给出的结果显示是有编码的 Profile 的:

Trying display: win32  
libva info: VA-API version 1.19.0  
libva info: User environment variable requested driver 'vaon12'  
libva info: Trying to open vaon12_drv_video.dll  
libva info: Found init function __vaDriverInit_1_19  
libva info: va_openDriver() returns 0  
vainfo.exe: VA-API version: 1.19 (libva 2.19.0)  
vainfo.exe: Driver version: Mesa Gallium driver 23.3.0-devel for D3D12 (AMD Radeon 780M Graphics)  
vainfo.exe: Supported profile and entrypoints  
     VAProfileH264ConstrainedBaseline: VAEntrypointVLD  
     VAProfileH264ConstrainedBaseline: VAEntrypointEncSlice  
     VAProfileH264Main               : VAEntrypointVLD  
     VAProfileH264Main               : VAEntrypointEncSlice  
     VAProfileH264High               : VAEntrypointVLD  
     VAProfileH264High               : VAEntrypointEncSlice  
     VAProfileHEVCMain               : VAEntrypointVLD  
     VAProfileHEVCMain10             : VAEntrypointVLD  
     VAProfileVP9Profile0            : VAEntrypointVLD  
     VAProfileVP9Profile2            : VAEntrypointVLD  
     VAProfileAV1Profile0            : VAEntrypointVLD  
     VAProfileNone                   : VAEntrypointVideoProc

不过我也懒得去理解这背后的原因究竟是什么,或者是尝试 AMD 的预览版显卡驱动了。本来到这里我已经快准备放弃了,但是突然一个邪恶的想法浮现在了我的脑内。

一条歪路

我注意到 Jellyfin 的转码依赖于 FFmpeg,而且是通过直接调用 FFmpeg 的方式实现的。已知:

  • 在我的 Windows 上,FFmpeg 能够正确调用硬件加速。
  • Windows 和 WSL2 的程序可以通过某种类似 NFS 的方式访问对方的文件系统。

那么,答案就呼之欲出了,我们制作一个假的 FFmpeg 可执行程序替代掉原有的 Jellyfin Docker 内的 FFmpeg,当 Jellyfin 调用该程序时,实际是 Windows 中的 Jellyfin 完成转码工作。要在 Docker 容器中调用宿主系统里的可执行程序没有一个现成的解决方案,我最开始试图利用 Bash 脚本和 FIFO Named Pipe 来做一个服务端和客户端,不过 Bash 编程实在过于反人类了,FIFO Named Pipe 不太便于侦测客户端是否还存活(当你退出视频播放时,Jellyfin 会向 FFmpeg 进程发送 SIGKILL 信号终止转码)。我最终决定用 C# 和 Unix domain socket 做一个简单的程序完成这项工作,它会从接受客户端从 socket 传来的请求,在宿主机上执行对应的程序,并将标准输出和标准错误输出的内容实时传回客户端,这样在 Jellyfin 眼中,它只是正常的执行了 FFmpeg。 代码很简单,可以从 https://github.com/Shuenhoy/SimpleRemoteExec 自行获取。

  • 注意: 该程序中没有任何校验环节,请自行确保使用环境安全可信。

最后,我们准备两个脚本,分别是在容器内的伪装 FFmpeg 的脚本:

#! /bin/bash  
exec /wsl_bridge/SimpleRemoteExec client /wsl_bridge/sock python ~/services/jellyfin/wsl_bridge/ffmpeg.py "$@"

以及在宿主系统中对 FFmpeg 的选项进行修改的脚本:

import sys  
import os  
bin="/mnt/c/Program Files/Jellyfin/Server/ffmpeg.exe"  
mapping = [  
   ["/nmedia", "~/services/jellyfin/media"],  
   ["/config", "~/services/jellyfin/config"]  
]  
def map_path(f: str):  
   for pair in mapping:  
       f = f.replace(pair[0],pair[1])  
   return f  
mapped = [map_path(x) for x in sys.argv]  
mapped[0] = bin  

if mapped[1] != '-h' and mapped[1] != '-version':  
   mapped = [mapped[0], "-hwaccel", "d3d11va",  *mapped[1:]]  

os.execv(mapped[0], mapped)

宿主系统里的脚本主要做了两件事情:

  • 路径转换,
  • 以及添加硬件解码。我将配置 Jellyfin 使用 AMF 进行硬件加速,但是 AMD AMF 并没有自己的解码功能,这里需要添加 Direct3D 11 的解码选项。

需要注意两个脚本中都使用 POSIX Exec 系统调用,直接替换原进程,以免后来收到 SIGKILL 信号的时候父进程被杀了子进程还在自己跑。另外,这里使用了 Jellyfin 自带的 FFmpeg,可以从 https://github.com/jellyfin/jellyfin-ffmpeg/releases 获得。实际上也可以使用其他的 FFmpeg,比如 winget install Gyan.FFmpeg,不过这时需要在 mapping 中添加一行 ["libfdk_aac", "aac"] 将 Jellyfin 默认指定的非自由 AAC 库改为 FFmpeg 内置库。

最后在 Jellyfin 中将硬件加速设置为 AMD AMF 就算完工了。

其他

不直接使用 Windows 版 Jellyfin 的原因。 我使用 qBittorrent 下载视频,之后通过 Sonarr 进行整理。在整理过程中,Sonarr 会使用硬链接的方式将视频命名为适合 Jellyfin 的格式。硬链接不能跨越文件系统,这意味着我必须将所有文件存在 Windows 上,除了转码和播放,其他部分的 IO 都要跨系统。而按照现在的配置,则只有转码需要跨系统。另外,这类装在系统里的服务也比 Docker 管理起来更困难一些。

目前的局限。 在使用 Direct3D 11 解码,使用 AMD AMF 编码的转码方式下,实际可以添加额外的选项 -hwaccel_output_format d3d11,这使视频内容被保留在显存中,不需要复制到内存中,从而获得更高的性能。可以用下面的方式验证:

ffmpeg -hwaccel d3d11va -hwaccel_output_format d3d11 -i input.mp4 -c:v h264_amf output.mkv
ffmpeg -hwaccel d3d11va -i input.mp4 -c:v h264_amf output.mkv

可以观察到前者的处理速度高于后者,从任务管理器则可看到前者 GPU 的 Video codec 占用率也更高。然而,当画面的尺寸变化或者像素格式(pix_fmt) 发生变化时,这种方式不可行。而 FFmpeg 的 AMD AMF 目前并不支持 Lolihouse 等压制组常使用的 HEVC 10bit 的 yuv420p10le 像素格式的编码,必须由软件进行像素格式的转变。除了等待相关补丁5的合并来引入 p10le 编码的支持。如果能够从 Jellyfin 给的命令判断出是否需要发生尺寸和像素格式的变化,从而自适应地插入选项。

另外一种可能的方案是利用前文提到的,Windows 下的 VA-API。VA-API 既有解码能力,又有编码能力。而且之前的 vainfo 的输出中存在 VAProfileHEVCMain10,这意味着它理论上可以进行 p10le 的编码。但是当我执行下面的命令后

ffmpeg -hwaccel vaapi -hwaccel_output_format vaapi -i input.mp4 -c:v hevc_vaapi output.mkv

它直接把我的显卡驱动搞出了某种问题。另外 VA-API 解码性能明显要差于 Direct3D 11。

Did you find apk for android? You can find new Free Android Games and apps.

关于作者

发表评论