功能:支持真实 2K 串流与重启持久化并优化渲染

变更详情:真2K串流与重启持久化,优化客户端渲染和帧率配置

三端统一2560x1440,盒子使用297MHz HDMI时序并保留1080p回退;Windows脚本动态定位VDD恢复2K。串流默认2K20/30Mbps,使用GLES2,设置页支持20/24FPS及自定义值。保持Windows正常光标,记录NVENC阶段性实测和GPU重启后Code43未恢复的限制。
This commit is contained in:
songyiming committed 2026-11-02 16:40:22 +08:00
1 parent 4f9565a768
commit e47b01beec
11 files changed
+198 -36

No files matched your search

+8 -8
View File
@@ -5,7 +5,7 @@
#
# host/port Sunshine 服务端地址与端口(当前联调环境为 VM 100)
# app 应用名。用名称而不是 app id:Sunshine 重装或升级后 app id 会变。
# width/height 分辨率,需与服务端显示模式匹配(当前虚拟显示器 1920x1080@60Hz)
# width/height 分辨率,需与服务端显示模式匹配(当前虚拟显示器 2560x1440@30Hz)
# fps 目标帧率
# bitrate 码率,单位 Kbps
# codec 视频编码:H.264 / HEVC / AV1 / auto
@@ -13,19 +13,19 @@
# RK3528 当前没有可用的 V4L2 request/m2m 设备,auto 会选到
# h264_v4l2m2m 并报 "Could not find a valid device",故用 software。
# vsync 1=开启垂直同步(--vsync),0=关闭(--no-vsync)
# 2026-10-29 A/B 实测:关闭 vsync/frame-pacing 对队列与渲染延迟
# 无改善(队列 47.2ms vs 基线 44.1ms),延迟瓶颈是服务端软编码
# 帧率(~21.6FPS,每帧约46ms),保持开启以获得更平滑的画面。
# 同步开关 A/B 未见明确延迟收益,保留开启以避免撕裂。
# 连续动画确认服务端可发送约59FPS;当前2K软件呈现选择20FPS档,
# NVENC重启故障及各档性能记录见 docs/moonlight-kiosk-20261028.md。
# frame-pacing 1=开启帧率匀速(--frame-pacing),0=关闭(--no-frame-pacing)
# performance-overlay 1=显示性能叠图(调试用)
# hang-timeout 串流假死判定时间(秒),0 表示关闭该检测
host=192.168.1.70
port=47984
app=Desktop
width=1920
height=1080
fps=60
bitrate=20000
width=2560
height=1440
fps=20
bitrate=30000
codec=H.264
decoder=software
display-mode=fullscreen
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
export DISPLAY="${DISPLAY:-:0}"
export XAUTHORITY="${XAUTHORITY:-/home/armbian/.Xauthority}"
# Match the local output to the 2K Moonlight stream.
# This kernel rejects the monitor's 241.70 MHz timing but accepts the 297 MHz
# PLL rate. Longer vertical blanking produces 2560x1440 at approximately 60 Hz.
# HDMI can appear a few seconds after the desktop autostart entry runs.
: >/tmp/vdi-display-mode.log
mode="2560x1440_60_fixed297"
for _ in $(seq 1 20); do
if xrandr --query 2>>/tmp/vdi-display-mode.log | grep -q '^HDMI-1 connected'; then
xrandr --newmode "$mode" 297.00 \
2560 2608 2640 2720 1440 1443 1448 1820 \
+HSync -VSync >>/tmp/vdi-display-mode.log 2>&1 || true
xrandr --addmode HDMI-1 "$mode" >>/tmp/vdi-display-mode.log 2>&1 || true
if xrandr --output HDMI-1 --mode "$mode" >>/tmp/vdi-display-mode.log 2>&1; then
break
fi
xrandr --output HDMI-1 --mode 1920x1080 --rate 60 \
>>/tmp/vdi-display-mode.log 2>&1 || true
fi
sleep 1
done
exec /home/armbian/vdi-terminal/bin/vdi-kiosk-monitor -- \
/home/armbian/vdi-terminal/bin/kiosk-shell
+6 -6
View File
@@ -30,9 +30,9 @@ struct DesktopInfo {
// 连接参数(kUnassigned 时为空)
QString endpoint; // 主机地址(IP / 域名 / VPN 隧道地址)
QString app; // Sunshine 应用名
int width = 1920;
int height = 1080;
int fps = 60;
int width = 2560;
int height = 1440;
int fps = 20;
};
// 一次会话的连接凭据;当前为本地构造,后续由平台 connect-ticket 接口下发
@@ -40,9 +40,9 @@ struct ConnectionTicket {
QString desktopId;
QString endpoint;
QString app;
int width = 1920;
int height = 1080;
int fps = 60;
int width = 2560;
int height = 1440;
int fps = 20;
};
struct AuthResult {
@@ -63,6 +63,8 @@ DesktopSettingsDialog::DesktopSettingsDialog(std::vector<DesktopInfo> desktops,
comboFps_ = new QComboBox(this);
comboFps_->addItem(QStringLiteral("60"), 60);
comboFps_->addItem(QStringLiteral("30"), 30);
comboFps_->addItem(QStringLiteral("24"), 24);
comboFps_->addItem(QStringLiteral("20"), 20);
form->addRow(QStringLiteral("帧率"), comboFps_);
root->addLayout(form);
@@ -109,7 +111,11 @@ void DesktopSettingsDialog::loadCurrent() {
QStringLiteral("%1x%2").arg(d.width).arg(d.height));
comboResolution_->setCurrentIndex(resIndex >= 0 ? resIndex : 0);
int fpsIndex = comboFps_->findData(d.fps);
comboFps_->setCurrentIndex(fpsIndex >= 0 ? fpsIndex : 0);
if (fpsIndex < 0) {
comboFps_->addItem(QString::number(d.fps), d.fps);
fpsIndex = comboFps_->count() - 1;
}
comboFps_->setCurrentIndex(fpsIndex);
loading_ = false;
break;
}
+6 -6
View File
@@ -23,9 +23,9 @@ std::vector<DesktopInfo> defaultDesktops(const QString& migratedServer) {
win.os = QStringLiteral("Windows 桌面");
win.endpoint = migratedServer; // 兼容老盒子:迁移 client.conf 的 server
win.app = QStringLiteral("Desktop");
win.width = 1920;
win.height = 1080;
win.fps = 60;
win.width = 2560;
win.height = 1440;
win.fps = 20;
list.push_back(win);
DesktopInfo ubuntu;
@@ -50,9 +50,9 @@ DesktopInfo fromJson(const QJsonObject& obj) {
d.os = obj.value(QStringLiteral("os")).toString();
d.endpoint = obj.value(QStringLiteral("endpoint")).toString();
d.app = obj.value(QStringLiteral("app")).toString();
d.width = obj.value(QStringLiteral("width")).toInt(1920);
d.height = obj.value(QStringLiteral("height")).toInt(1080);
d.fps = obj.value(QStringLiteral("fps")).toInt(60);
d.width = obj.value(QStringLiteral("width")).toInt(2560);
d.height = obj.value(QStringLiteral("height")).toInt(1440);
d.fps = obj.value(QStringLiteral("fps")).toInt(20);
d.state = d.endpoint.isEmpty() ? DesktopState::kUnassigned : DesktopState::kAvailable;
return d;
}
@@ -123,7 +123,12 @@ bool MoonlightSession::activateStreamWindow() {
{QStringLiteral("search"), QStringLiteral("--onlyvisible"),
QStringLiteral("--pid"), QString::number(process_->processId()),
QStringLiteral("--class"), QStringLiteral("com.moonlight_stream.Moonlight")});
if (!finder.waitForFinished(500) || finder.exitCode() != 0) {
if (!finder.waitForFinished(500)) {
finder.kill();
finder.waitForFinished();
return false;
}
if (finder.exitCode() != 0) {
return false;
}
const QString windowId = QString::fromUtf8(finder.readAllStandardOutput())
@@ -154,8 +159,10 @@ bool MoonlightSession::start(const ConnectionTicket& ticket, EventCallback callb
process_->setProgram(QStringLiteral("moonlight-qt"));
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
environment.insert(QStringLiteral("GL_IS_SLOW"), QStringLiteral("0"));
environment.insert(QStringLiteral("SDL_RENDER_DRIVER"), QStringLiteral("opengles2"));
process_->setProcessEnvironment(environment);
const int bitrateKbps = ticket.width >= 2560 ? 30000 : 20000;
QStringList args;
args << QStringLiteral("stream")
<< ticket.endpoint
@@ -163,7 +170,7 @@ bool MoonlightSession::start(const ConnectionTicket& ticket, EventCallback callb
<< QStringLiteral("--resolution")
<< QStringLiteral("%1x%2").arg(ticket.width).arg(ticket.height)
<< QStringLiteral("--fps") << QString::number(ticket.fps)
<< QStringLiteral("--bitrate") << QString::number(20000)
<< QStringLiteral("--bitrate") << QString::number(bitrateKbps)
<< QStringLiteral("--display-mode") << QStringLiteral("fullscreen")
<< QStringLiteral("--video-codec") << options_.videoCodec
<< QStringLiteral("--video-decoder") << options_.decoder
+1 -1
View File
@@ -2,7 +2,7 @@
Type=Application
Name=VDI Cloud Desktop (Kiosk)
Comment=产品唯一自启入口:看门狗守护 kiosk-shell(登录 -> 桌面列表 -> 会话)
Exec=/home/armbian/vdi-terminal/bin/vdi-kiosk-monitor -- /home/armbian/vdi-terminal/bin/kiosk-shell
Exec=/home/armbian/vdi-terminal/bin/vdi-kiosk-start
Terminal=false
X-GNOME-Autostart-enabled=true
X-GNOME-Autostart-Delay=2