Ubuntu Server 24.10 在 N100 小主机上最小化安装 kodi

首图是目前的家庭网络设备拓扑图。虽然是教程贴,还是记录下前因后果。

看片困境

前段时间买了低功耗小主机,关掉了威联通 NAS,客厅如何看电视就成了问题,看了不少经验分享贴都推荐五十块的二手魔百盒。闲鱼下单了 s905l3a 带 wifi 的商家版本,预装了常用的电视 app,配合红外遥控确实是老年人友好的解决方案。
开机后发现按键操作时常失灵,而且操作和播放视频过程中频繁自启或者退回桌面。联系商家说可能是电源的问题,这个是旧华为路由器闲置下来的。尝试跟光猫的电源(高 0.5A)交换,情况有所好转,但依然会概率性重启。一番交涉,商家爽快的答应换货。

最坑的是,BBLL 能装上,jellyfin TV 却不行。网上也看到有人 po 出同样的问题,原因是这种在运营商基础上魔改的 ROM 缺少很多系统模块。有大佬给出了 patch,但需要自己动手编译并且不保证有 bug,遂放弃。
当然,还有刷机这条路,买这个盒子也是图他 ROM 生态丰富。偶然间看到盒子店铺的一条评论,想再刷回来的话还要再掏钱或者提前备份镜像(不会啊),还是放弃吧。所以换货回来只是简单检查了下重启的毛病,就没有继续折腾的兴致了,扔到卧室给小朋友放放动画片也挺好。

前期摸索

所以事情又回到原点,客厅的电视用什么播放器。最终,还是打上了小主机的主意。因为一开始只打算做网络服务,装的是没有桌面程序的 Ubuntu server 系统,不过 N100 算比较新的 CPU,为了提高驱动兼容性上了最新的 **Ubuntu 24.10 (Oracular Oriole)**,据说明年 Plucky Puffin 发布的时候可以无痛升级。

所有 linux TV 播放器里面,kodi 绝对是老大哥,生态完整、资料好找,安装方式也多种多样,最简单的当然是 libreElec,开箱即用,但考虑到要把系统重装为 PVE,也没有 openwrt 的需求,还是打算宿主机直装的方式。一开始考虑 docker,但不是版本太老就是没啥人用,出问题的概率很大。

幸运的是,找到两篇(12)在 server 版 ubuntu 上最小安装 kodi 的教程;遗憾的是,他们用的 GUI 框架都是比较古老的 X11,在最新内核下需要安装很多依赖并且参在兼容性风险。另外这个一键安装脚本也是差不多的思路。关于 X11、Wayland 和 GBM 的区别可以看这里的解释。

进一步查找,官方论坛的这个帖子给出了重要线索,所以首先确认硬件驱动没问题(参考 12),然后根据官方文档直接安装。

具体流程

硬件和驱动情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
➜  ~ uname -a
Linux n100 6.11.0-9-generic #9-Ubuntu SMP PREEMPT_DYNAMIC Mon Oct 14 13:19:59 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
➜ ~ sudo lspci -v | grep i915
Kernel driver in use: i915
Kernel modules: i915, xe
➜ ~ sudo lshw | grep i915
configuration: depth=32 driver=i915 latency=0 mode=1920x1080 resolution=1920,1080 visual=truecolor xres=1920 yres=1080
➜ ~ sudo dmesg | grep i915
[ 13.326033] i915 0000:00:02.0: [drm] Found ALDERLAKE_P/ADL-N (device ID 46d1) display version 13.00 stepping D0
[ 13.326927] i915 0000:00:02.0: [drm] VT-d active for gfx access
[ 13.370261] i915 0000:00:02.0: vgaarb: deactivate vga console
[ 13.371357] i915 0000:00:02.0: [drm] Using Transparent Hugepages
[ 13.371930] i915 0000:00:02.0: vgaarb: VGA decodes changed: olddecodes=io+mem,decodes=io+mem:owns=io+mem
[ 13.374690] i915 0000:00:02.0: [drm] Finished loading DMC firmware i915/adlp_dmc.bin (v2.20)
[ 13.587503] i915 0000:00:02.0: [drm] GT0: GuC firmware i915/tgl_guc_70.bin version 70.29.2
[ 13.587515] i915 0000:00:02.0: [drm] GT0: HuC firmware i915/tgl_huc.bin version 7.9.3
[ 13.592056] i915 0000:00:02.0: [drm] GT0: HuC: authenticated for all workloads
[ 13.592757] i915 0000:00:02.0: [drm] GT0: GUC: submission enabled
[ 13.592762] i915 0000:00:02.0: [drm] GT0: GUC: SLPC enabled
[ 13.593204] i915 0000:00:02.0: [drm] GT0: GUC: RC enabled
[ 13.594830] mei_pxp 0000:00:16.0-fbf6fcf1-96cf-4e2e-a6a6-1bab8cbe36b1: bound 0000:00:02.0 (ops i915_pxp_tee_component_ops [i915])
[ 13.595027] i915 0000:00:02.0: [drm] Protected Xe Path (PXP) protected content support initialized
[ 13.595034] mei_hdcp 0000:00:16.0-b638ab7e-94e2-4ea2-a552-d1c54b627f04: bound 0000:00:02.0 (ops i915_hdcp_ops [i915])
[ 13.630189] [drm] Initialized i915 1.6.0 for 0000:00:02.0 on minor 1
[ 13.706559] fbcon: i915drmfb (fb0) is primary device
[ 13.784629] i915 0000:00:02.0: [drm] fb0: i915drmfb frame buffer device
[ 13.796441] sof-audio-pci-intel-tgl 0000:00:1f.3: bound 0000:00:02.0 (ops i915_audio_component_bind_ops [i915])

看上去万事具备了,不需要手动安装驱动之类的。根据官方文档add-apt-repository -r ppa:team-xbmc/ppa却遇到问题,提示“The repository ‘https://ppa.launchpadcontent.net/team-xbmc/ppa/ubuntu oracular Release’ does not have a Release file”,手动修改/etc/apt/sources.list.d/team-xbmc-ubuntu-ppa-oracular.sources,把oracular 降级为 23.04 的lunar是可以获取到源了,但却少了kodi-gbm包,且不知道会不会有其他隐性问题。从新闻看到,kodi 团队从今年 5 月开始就停止维护 PPA 源了,索性参考文档手动编译。另外根据 reddit 网友提供的信息,手动编译可以设置 HDR 直通(虽然家里的老电视只支持 SDR)。

同样的问题,安装前置依赖过程中无法添加 xbmc-nightly 源,只能手动安装,大概占掉 1G 的硬盘空间。配置之前还要安装 libdisplay-info:

cd ~ && wget https://gitlab.freedesktop.org/emersion/libdisplay-info/-/archive/0.2.0/libdisplay-info-0.2.0.tar.gz
tar xzf libdisplay-info-0.2.0.tar.gz && cd libdisplay-info-0.2.0
mkdir build && cd build
meson setup --prefix=/usr --buildtype=release
ninja
sudo ninja install
cd ~/kodi-build
cmake ../kodi -DCMAKE_INSTALL_PREFIX=/usr/local -DCORE_PLATFORM_NAME=gbm -DAPP_RENDER_SYSTEM=gles -DENABLE_VAAPI=ON

理所当然的报错了:

CMake Error at /usr/share/cmake-3.30/Modules/FindPackageHandleStandardArgs.cmake:233 (message):
Could NOT find PCRE (missing: PCRE_LIBRARY PCRE_INCLUDE_DIR)
Call Stack (most recent call first):
/usr/share/cmake-3.30/Modules/FindPackageHandleStandardArgs.cmake:603 (_FPHSA_FAILURE_MESSAGE)
cmake/modules/FindPCRE.cmake:126 (find_package_handle_standard_args)
cmake/scripts/common/Macros.cmake:403 (find_package)
cmake/scripts/common/Macros.cmake:417 (find_package_with_ver)
CMakeLists.txt:261 (core_require_dep)

-- Configuring incomplete, errors occurred!

搜索发现,最近的更新从 pcre 切换到了 pcre2,而之前安装的就是 libpcre2-dev,而 ubuntu apt 仓库里根本就没有 libpcre-dev。所以源码下载地址从release 切换到master。顺利通过配置,执行编译,然后安装必要插件并执行安装。

cmake --build . -- VERBOSE=1 -j$(getconf _NPROCESSORS_ONLN)
cd ~/kodi
sudo make -j$(getconf _NPROCESSORS_ONLN) -C tools/depends/target/binary-addons PREFIX=/usr/local ADDONS="audioencoder.flac audioencoder.lame audioencoder.vorbis audioencoder.wav"
sudo make install
cd ../kodi-build && sudo make install

注意编译过程中会从网络下载依赖,编译期间最好挂上梯子。

主程序编译耗时 30 分钟,成功之后生成 kodi-gbm 可执行文件,使用 sudo ./kodi-gbm进行测试。播放压制视频时 CPU 占用在 10%~45% 之间,拖动丝滑。注意,如果没有给当前用户赋予足够的权限(特别是 render 和 input 群组),一定要用 root 执行,否则可能无法硬解或使用键鼠。成功执行后会在用户根目录下生成 .kodi 文件夹,用于存放程序相关数据。

后续问题

远程遥控 app

手机上安装 Yatse 并连上局域网,小主机插上鼠标,根据文档打开远程控制功能。成功登录之后就可以拔掉鼠标或键盘了。Pro 版增加了投屏、jellyfin/emby/plex 客户端、离线播放等功能,仅售 3.49 刀。经测试,实际是调用 vlc player 或 MX player 进行播放,本身只是海报墙的作用。DLNA 的转码功能要另外收费,另外云转码也不是很实用。所以值不值就见仁见智了。

另外比较坑的是电源菜单第一项是直接关闭主机,而不是退出程序。kodi 没有运行的时候,电源菜单只能通过远程唤醒(WoL)的方式开机(所以要设置 kodi 开机启动)。而我想要的效果是:kodi 服务停止时,电源键可以远程执行systemctl start命令启动服务;kodi 运行时,电源键默认退出 app 而不是关机。

kodi 跟随电视启停

原本考虑监听 WoL 端口,主机开机状态下收到特征 udp 包则启动 kodi,然而还是不太优雅。于是萌生了另一个想法:轮询判断电视的电源状态,从而启动/关闭 kodi。

这里的关键是/sys/class/drm/card0-HDMI-A-2/status文件的值。card0 代表显卡,重启后可能变成 card1。HDMI 是接口类型,根据实际情况可能是 DP。A-2代表接口编号,比如A-1是 type-c 接口。执行关闭时采用 jsonapi http post 的方式以便安全退出;启动则直接使用 systemctl 命令。根据 AI 给出的答案,首先创建 bash 文件并给予执行权限:

/usr/local/bin/hdmi-monitor.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/bin/bash
# /usr/local/bin/hdmi-monitor.sh

KODI_HOST="usr:pwd@localhost"
KODI_PORT="8080"
LOG_FILE="/var/log/hdmi-monitor.log"
MAX_RETRIES=2
RETRY_DELAY=2

log_message() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $message" >> "$LOG_FILE"
}

check_kodi_available() {
local timeout=1
curl -s -I -m $timeout "http://$KODI_HOST:$KODI_PORT/jsonrpc" >/dev/null
return $?
}

send_alert() {
local title="$1"
curl -s -m 30 -X POST "https://cloudflare.notice.service" -d "sp=weixin&t=$message" >/dev/null
systemctl stop hdmi-monitor.timer
}

send_kodi_command() {
local method="$1"
local retry_count=0
local success=false

while [ $retry_count -lt $MAX_RETRIES ] && [ "$success" = false ]; do
if ! check_kodi_available; then
log_message "Kodi HTTP server not responding, attempt $(($retry_count + 1))/$MAX_RETRIES"
sleep $RETRY_DELAY
retry_count=$((retry_count + 1))
continue
fi

response=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"id\":1}" \
"http://$KODI_HOST:$KODI_PORT/jsonrpc")

if [ $? -eq 0 ] && [ ! -z "$response" ]; then
if echo "$response" | grep -q "error"; then
log_message "Error executing Kodi command $method: $response"
else
# log_message "Successfully executed Kodi command: $method"
success=true
break
fi
else
log_message "Failed to execute Kodi command $method, attempt $(($retry_count + 1))/$MAX_RETRIES"
fi

retry_count=$((retry_count + 1))
[ $retry_count -lt $MAX_RETRIES ] && sleep $RETRY_DELAY
done

if [ "$success" = false ]; then
log_message "Failed to execute Kodi command $method after $MAX_RETRIES attempts"
return 1
fi
return 0
}

stop_kodi_gracefully() {
log_message "Attempting to stop Kodi gracefully"

# Quit the application
if ! send_kodi_command "Application.Quit"; then
log_message "Error: Failed to quit Kodi gracefully, forcing service stop"
else
sleep 2
fi

# Finally stop the service
if systemctl is-active --quiet kodi-gbm.service; then
if ! systemctl stop kodi-gbm.service; then
log_message "Error: Failed to stop kodi-gbm.service"
return 1
fi
fi
log_message "Successfully stopped Kodi and its service"
return 0
}

# Ensure log file exists and is writable
touch "$LOG_FILE" 2>/dev/null || {
echo "Error: Cannot create or access log file at $LOG_FILE"
exit 1
}

# Try to read status from card0 first, then card1 if that fails
STATUS=$(cat "/sys/class/drm/card0-HDMI-A-2/status" 2>/dev/null)
CARD="card0"

if [ $? -ne 0 ]; then
STATUS=$(cat "/sys/class/drm/card1-HDMI-A-2/status" 2>/dev/null)
CARD="card1"
if [ $? -ne 0 ]; then
log_message "Error: Could not read HDMI status card0 or card1"
send_alert "KODI_HDMI_FAIL"
exit 1
fi
fi

if [ "$STATUS" = "connected" ]; then
if ! systemctl is-active --quiet kodi-gbm.service; then
if ! systemctl start kodi-gbm.service; then
log_message "Error: Failed to start kodi-gbm.service"
send_alert "KODI_START_FAIL"
exit 1
fi
log_message "Successfully start Kodi"
fi
else
if systemctl is-active --quiet kodi-gbm.service; then
if ! stop_kodi_gracefully; then
exit 1
fi
fi
fi

exit 0

然后是 service 相关文件:

/etc/systemd/system/hdmi-monitor.service
[Unit]
Description=HDMI Connection Monitor Service
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/hdmi-monitor.sh

[Install]
WantedBy=multi-user.target
/etc/systemd/system/hdmi-monitor.timer
[Unit]
Description=Timer for HDMI Connection Monitor Service

[Timer]
OnBootSec=30
OnUnitActiveSec=5s
Unit=hdmi-monitor.service

[Install]
WantedBy=timers.target
/etc/logrotate.d/hdmi-monitor
/var/log/hdmi-monitor.log {
weekly
rotate 4
compress
missingok
notifempty
}

启动服务并设置开机自启:

sudo systemctl daemon-reload
sudo systemctl enable hdmi-monitor.timer
sudo systemctl start hdmi-monitor.timer
### check service
sudo systemctl status hdmi-monitor.timer

shellCrash 白名单

如果自定义了保留字段,需要把所有局域网段都添加进去,否则本地也有可能走核心。详见讨论

播放无声音

如果运行 kodi 用户添加了 audio 用户组还是没办法调出声音,那么有可能是音频服务组件缺失,具体表现为 audio output device 列表中只有一项 sof-hda-dsp, Analog。首先安装 alsa 包,依次检查耳机和 HDMI 能否发出声音:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
➜  ~ asudo apt install alsa-base
### Check volume
➜ ~ asudo alsamixer
### Check Hardware Devices
➜ ~ asudo aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: sofhdadsp [sof-hda-dsp], device 0: HDA Analog (*) []
Subdevices: 1/1
Subdevice #0: subdevice #0
card 0: sofhdadsp [sof-hda-dsp], device 3: HDMI1 (*) []
Subdevices: 0/1
Subdevice #0: subdevice #0
card 0: sofhdadsp [sof-hda-dsp], device 4: HDMI2 (*) []
Subdevices: 1/1
Subdevice #0: subdevice #0
card 0: sofhdadsp [sof-hda-dsp], device 5: HDMI3 (*) []
Subdevices: 1/1
Subdevice #0: subdevice #0
card 0: sofhdadsp [sof-hda-dsp], device 31: HDA Analog Deep Buffer (*) []
Subdevices: 1/1
Subdevice #0: subdevice #0
### test speaker though headphone
sudo aplay /usr/share/sounds/alsa/*
### test speaker though HDMI
sudo speaker-test --channels 2 --test wav --device hw:0,3

如果通过测试,参考这篇文章安装 pipewire(ubuntu 22.04 之后逐步替代 pulseaudio)。注意,pipewire-pulse 可以不装,这个是为了兼容不支持 pipewire 的旧 app,所以:

sudo add-apt-repository ppa:pipewire-debian/pipewire-upstream
sudo apt install pipewire pipewire-audio-client-libraries gstreamer1.0-pipewire libspa-0.2-bluetooth libspa-0.2-jack
systemctl --user enable pipewire.socket
systemctl --user start pipewire.socket

注意,这里不可以使用 root 身份执行。

standalone 模式

参考graysky2/kodi-standalone-service进行设置,需要根据实际情况修改几处:

  1. x86/init/kodi-gbm.service14 行的 ExecStart 修改为正确路径,比如/usr/local/bin/kodi-standalone
  2. x86/init/sysusers.conf取消第 22、26 行注释,增加第 9 行注释,增加m kodi inputm kodi plugdev

arctic fuse 2 皮肤

jurialmunkey 大神的经典 kodi 皮肤,前代已经不更新。安装之前记得先到插件设置里开启第三方插件的安装和更新权限,否则无法安装 TheMovieDB Helper 依赖,方法如下

具体安装步骤就不说了,网上一搜一大把,建议通过 repository 源安装。

最新版(v2.4.21)已经支持中文。但!是!不出意外的话少数汉字会变成方块,比如下图里的“管”、“年”

主要原因是 kodi 自带的 CJK 字体存在缺字漏字,可以看这个 issue 里的讨论。

解决办法当然是换字体,参考官方文档

以霞骛文楷为例,去 lxgw 拉取最新的 TTF 文件,为了防止超出 kodi 对外挂字体的限制选择 lite 版,应付日常足够了。记得把 Light、Medium、Regular 都下载下来,保存到 .kodi/media/fonts(全局)或.kodi\addons\skin.arctic.fuse.2\fonts(仅本皮肤生效)下面。.kodi 文件夹一般在用户根目录下,比如/storage/.kodi,找不到的话可以查官方文档

然后修改.kodi/addons/skin.arctic.fuse.2/1080i/Font.xml,在</fonts>标签上面添加几行:

<fontset id="LxgwWenkaiGB-Lite" unicode="true">
<include content="Font_Default">
<param name="font_black">LXGWWenKaiGBLite-Medium.ttf</param>
<param name="font_bold">LXGWWenKaiGBLite-Medium.ttf</param>
<param name="font_regular">LXGWWenKaiGBLite-Regular.ttf</param>
<param name="font_light">LXGWWenKaiGBLite-Light.ttf</param>
</include>
</fontset>

id的值就是字体显示的名称,然后在设置中修改皮肤字体,如下图

字体问题是解决了,但是跟 jellyfin 插件还是存在一些兼容性问题,比如无法显示文件详细资料

问题不大,后面再慢慢改进吧。