重构:移除 RDP 管线并完善会话退出与串流过场

变更详情:删除 RDP 渲染管线,协议统一 Moonlight;退出三层设计与串流过场层落地

- 移除 rdp_session.*、FreeRDP 构建依赖与代码内协议概念,Moonlight 为唯一会话通道
- Ctrl+Alt+Shift+Q 退串流回列表(kiosk 兜底 + moonlight-qt 主路径);登录页去退出按钮
- 新增 TransitionOverlay:全屏过场层盖住串流启动闪烁,进程 CPU 首帧检测后淡出
- Sunshine 写入 sw_preset=ultrafast;实测推翻 x264 瓶颈结论(21.6FPS 帽子在虚拟显示/捕获节奏,CPU≈0%)
- 文档:麒麟(101/.71)/Ubuntu(102/.72) 接入信息与四件事落地记录;MEMORY 根因修正
This commit is contained in:
songyiming committed 2026-10-29 17:37:38 +08:00
1 parent 56b9f1faf3
commit 26fde0369e
21 files changed
+364 -923

No files matched your search

+72 -344
View File
@@ -2,77 +2,27 @@
#include <QApplication>
#include <QDateTime>
#include <QEvent>
#include <QFile>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPoint>
#include <QScreen>
#include <QShortcut>
#include <QStackedWidget>
#include <QStatusBar>
#include <QTimer>
#include <QVBoxLayout>
#include <cstring>
#include <freerdp/codec/color.h>
#include <freerdp/locale/keyboard.h>
#include <freerdp/scancode.h>
#include "desktop_list_page.h"
#include "desktop_settings_dialog.h"
#include "login_page.h"
#include "transition_overlay.h"
namespace vdi {
namespace {
// FreeRDP 像素格式 -> QImage::Format
// 注意:FreeRDP 的像素格式值是计算宏(FREERDP_PIXEL_FORMAT),
// 不能硬编码数值,必须用宏直接比较。
// FreeRDP 主缓冲是 4 字节/像素(BGRX / BGRA / RGBA),
// QImage 没有对应的 4 字节 BGRA 格式,因此统一转成 RGB888 输出。
QImage::Format toQImageFormat(uint32_t freerdpFormat) {
(void)freerdpFormat;
return QImage::Format_RGB888;
}
// FreeRDP BGRX32 在当前 ARM64 小端目标上与 QImage::Format_RGB32 的内存布局一致。
QImage convertFrame(const uint8_t* data, uint32_t width, uint32_t height, uint32_t bytesPerLine,
uint32_t freerdpFormat) {
if (!data || width == 0 || height == 0) {
return QImage();
}
if (freerdpFormat == PIXEL_FORMAT_BGRX32 || freerdpFormat == PIXEL_FORMAT_BGRA32) {
QImage out(static_cast<int>(width), static_cast<int>(height), QImage::Format_RGB32);
const size_t rowBytes = static_cast<size_t>(width) * 4;
for (uint32_t y = 0; y < height; ++y) {
std::memcpy(out.scanLine(static_cast<int>(y)),
data + static_cast<size_t>(y) * bytesPerLine, rowBytes);
}
return out;
}
QImage out(static_cast<int>(width), static_cast<int>(height), QImage::Format_RGB888);
for (uint32_t y = 0; y < height; ++y) {
const uint8_t* src = data + static_cast<size_t>(y) * bytesPerLine;
uint8_t* dst = out.scanLine(static_cast<int>(y));
for (uint32_t x = 0; x < width; ++x) {
const uint8_t* p = src + static_cast<size_t>(x) * 4;
dst[0] = p[0];
dst[1] = p[1];
dst[2] = p[2];
dst += 3;
}
}
return out;
}
constexpr int kReconnectDelayMs = 3000;
constexpr int kMaxReconnectAttempts = 5;
// 首帧检测:moonlight-qt 是独立进程,kiosk 拿不到解码回调。
// 软解 1080p60 的 CPU 占用远高于启动握手期,因此轮询进程 CPU 时间,
// 累计超过阈值即认为串流已出画;超时则强制淡出兜底(时钟频率 100 tick/秒)。
constexpr int kFirstFramePollMs = 500;
constexpr qint64 kFirstFrameCpuTicks = 120;
constexpr qint64 kFirstFrameTimeoutMs = 15000;
} // namespace
@@ -83,12 +33,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setupUi();
setupHotkey();
setupHeartbeat();
keyboardLayout_ = freerdp_keyboard_init(0);
config_ = loadConfig();
// 开机永远进入登录页;不再根据本地配置自动连接 RDP,
// 会话只能由用户在桌面列表选择后启动。
// 开机永远进入登录页;会话只能由用户在桌面列表选择后启动。
fsm_.transitionTo(State::kConfig, QStringLiteral("等待登录"));
showLogin();
}
@@ -120,7 +68,6 @@ void MainWindow::setupUi() {
stack_->addWidget(login);
connect(login, &LoginPage::loginRequested, this, &MainWindow::onLoginRequested);
connect(login, &LoginPage::quitRequested, this, &MainWindow::onQuitRequested);
// 桌面列表页
auto* list = new DesktopListPage(this);
@@ -136,64 +83,44 @@ void MainWindow::setupUi() {
connect(list, &DesktopListPage::editRequested, this, &MainWindow::onEditDesktopRequested);
connect(list, &DesktopListPage::logoutRequested, this, &MainWindow::onLogoutRequested);
// 会话视图
auto* sessionWidget = new QWidget(this);
auto* sessionLayout = new QVBoxLayout(sessionWidget);
sessionLayout->setContentsMargins(0, 0, 0, 0);
sessionView_ = new QLabel(sessionWidget);
sessionView_->setFocusPolicy(Qt::StrongFocus);
sessionView_->setMouseTracking(true);
sessionView_->installEventFilter(this);
sessionView_->setAlignment(Qt::AlignCenter);
sessionView_->setStyleSheet(QStringLiteral("background: #000000; color: #e6e9ef;"));
sessionView_->setText(QStringLiteral("正在建立远程会话…"));
sessionOverlay_ = new QLabel(sessionWidget);
sessionOverlay_->setAlignment(Qt::AlignCenter);
sessionOverlay_->setStyleSheet(
QStringLiteral("background: rgba(15,20,32,0.82); color: #e6e9ef; "
"font-size: 20px; padding: 40px;"));
sessionOverlay_->setText(QStringLiteral("正在连接…"));
sessionOverlay_->hide();
sessionLayout->addWidget(sessionView_);
sessionLayout->addWidget(sessionOverlay_);
stack_->addWidget(sessionWidget);
// 过场层:独立置顶窗口,盖住 moonlight-qt 启动期的小窗口闪烁与分辨率切换,
// 首帧检测到后淡出。不属于 stack_(串流期间整个 kiosk 都被 moonlight 覆盖)。
overlay_ = new TransitionOverlay(this);
// 状态条
statusBar_ = new QStatusBar(this);
statusBar_->setStyleSheet(QStringLiteral("QStatusBar { background: #161d2e; color: #8a94a6; }"));
setStatusBar(statusBar_);
// RDP 重连定时器
reconnectTimer_ = new QTimer(this);
reconnectTimer_->setSingleShot(true);
connect(reconnectTimer_, &QTimer::timeout, this, [this]() {
if (fsm_.current() == State::kReconnect && reconnectAttempts_ < kMaxReconnectAttempts) {
++reconnectAttempts_;
if (activeTicket_.protocol == Protocol::kMoonlight) {
startMoonlightSession(activeTicket_);
} else {
startRdpSession(activeTicket_);
}
} else if (fsm_.current() == State::kReconnect) {
stopSessions();
fsm_.transitionTo(State::kDesktopList, QStringLiteral("重连失败,已返回桌面列表"));
}
});
frameRefreshTimer_ = new QTimer(this);
frameRefreshTimer_->setInterval(33);
connect(frameRefreshTimer_, &QTimer::timeout, this, &MainWindow::onFrameRefresh);
frameRefreshTimer_->start();
// 串流首帧检测
firstFrameTimer_ = new QTimer(this);
firstFrameTimer_->setInterval(kFirstFramePollMs);
connect(firstFrameTimer_, &QTimer::timeout, this, &MainWindow::watchFirstFrame);
}
void MainWindow::setupHotkey() {
// 全局热键 Ctrl+Alt+Shift+M 呼出管理菜单(方案 3.2 节)
auto* shortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+M")), this);
shortcut->setContext(Qt::ApplicationShortcut);
connect(shortcut, &QShortcut::activated, this, [this]() { onMenuAction(0); });
auto* menuShortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+M")), this);
menuShortcut->setContext(Qt::ApplicationShortcut);
connect(menuShortcut, &QShortcut::activated, this, [this]() { onMenuAction(0); });
// Ctrl+Alt+Shift+Q 退出串流回桌面列表。
// 主路径是 moonlight-qt 自带的同一快捷键(串流独占键盘时由它退出,
// 进程退出后 onMoonlightState 自动回列表);这里兜底处理
// 串流尚未建立、假死或焦点仍在 kiosk 的情况。
auto* quitShortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+Q")), this);
quitShortcut->setContext(Qt::ApplicationShortcut);
connect(quitShortcut, &QShortcut::activated, this, [this]() {
if (fsm_.isIn(State::kConnecting) || fsm_.isIn(State::kSession) ||
fsm_.isIn(State::kReconnect)) {
stopSessions();
fsm_.transitionTo(State::kDesktopList, QStringLiteral("已退出串流"));
if (desktopListPage_) {
desktopListPage_->setBusy(false);
desktopListPage_->setStatus(QStringLiteral("已退出串流"));
}
}
});
}
void MainWindow::setupHeartbeat() {
@@ -228,21 +155,14 @@ void MainWindow::onStateChanged(State from, State to, const QString& detail) {
refreshDesktopList();
break;
case State::kConnecting:
if (stack_->currentWidget() != sessionView_->parentWidget()) {
stack_->setCurrentWidget(sessionView_->parentWidget());
}
sessionView_->setFocus(Qt::OtherFocusReason);
sessionOverlay_->setText(QStringLiteral("正在连接 %1 …").arg(activeTicket_.endpoint));
sessionOverlay_->show();
firstFrameClock_.restart();
overlay_->start(QStringLiteral("正在进入云桌面"));
firstFrameTimer_->start();
break;
case State::kSession:
sessionView_->setFocus(Qt::OtherFocusReason);
sessionOverlay_->hide();
// 串流画面由 moonlight-qt 全屏呈现;过场层由首帧检测负责淡出
break;
case State::kReconnect:
sessionOverlay_->setText(QStringLiteral("连接已断开,正在重连…"));
sessionOverlay_->show();
reconnectTimer_->start(kReconnectDelayMs);
break;
case State::kError:
stopSessions();
@@ -322,18 +242,12 @@ void MainWindow::onDesktopSelected(const QString& desktopId) {
return;
}
activeTicket_ = ticket;
reconnectAttempts_ = 0;
if (ticket.protocol == Protocol::kMoonlight) {
startMoonlightSession(ticket);
} else {
startRdpSession(ticket);
}
startMoonlightSession(ticket);
});
}
void MainWindow::onEditDesktopRequested(const QString& desktopId) {
// 桌面连接设置:编辑协议/地址/应用名/分辨率/帧率,保存后立即刷新列表。
// 桌面连接设置:编辑地址/应用名/分辨率/帧率,保存后立即刷新列表。
// 现场环境地址不同或走 VPN 隧道时,在这里改地址即可,无需改代码。
DesktopSettingsDialog dialog(desktops_, desktopId, this);
dialog.setStyleSheet(
@@ -361,81 +275,10 @@ void MainWindow::onLogoutRequested() {
fsm_.transitionTo(State::kConfig, QStringLiteral("已退出登录"));
}
void MainWindow::startRdpSession(const ConnectionTicket& ticket) {
++sessionEpoch_;
{
QMutexLocker locker(&frameMutex_);
latestFrame_ = QImage();
frameEpoch_ = sessionEpoch_;
frameWakePending_ = false;
}
if (fsm_.current() == State::kSession) {
fsm_.transitionTo(State::kReconnect, QStringLiteral("手动重新连接"));
}
if (!fsm_.transitionTo(State::kConnecting, ticket.endpoint)) {
if (fsm_.current() != State::kConnecting) {
return;
}
}
RdpOptions opts;
opts.server = ticket.endpoint.toStdString();
opts.username = ticket.username.toStdString();
opts.password = ticket.password.toStdString();
opts.keyboardLayout = keyboardLayout_;
opts.width = ticket.width;
opts.height = ticket.height;
opts.bpp = 32;
opts.fullscreen = true;
// 与 xfreerdp3 相同的 GFX 能力组合语义:
// FreeRDP 3.32.1 不接受单个管道名,必须成组。
opts.gfx = "progressive|RFX|AVC420|AVC444";
opts.ignoreCertificate = true; // POC 阶段;正式产品必须做证书校验
RdpCallbacks cb;
cb.onFrame = [this](const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride,
uint32_t format) {
// 在 FreeRDP 线程中复制帧,不能把其内部缓冲区裸指针排队到 GUI 线程。
const QImage image = convertFrame(data, width, height, stride, format);
if (image.isNull()) {
return;
}
QMutexLocker locker(&frameMutex_);
latestFrame_ = image;
frameEpoch_ = sessionEpoch_;
if (frameWakePending_) {
return;
}
frameWakePending_ = true;
locker.unlock();
QMetaObject::invokeMethod(this, &MainWindow::onFrameWakeup, Qt::QueuedConnection);
};
cb.onState = [this](bool connected, const std::string& message) {
QMetaObject::invokeMethod(
this,
[this, connected, message]() {
onSessionState(connected, QString::fromStdString(message));
},
Qt::QueuedConnection);
};
rdpSession_ = std::make_unique<RdpSession>();
rdpSession_->start(opts, cb);
}
void MainWindow::startMoonlightSession(const ConnectionTicket& ticket) {
if (!fsm_.transitionTo(State::kConnecting, ticket.endpoint)) {
if (fsm_.current() != State::kConnecting) {
return;
}
return;
}
// Moonlight 是外部全屏进程:Kiosk 画面停在"正在连接"覆盖层,
// 串流窗口由 moonlight-qt 自行置顶显示。
if (stack_->currentWidget() != sessionView_->parentWidget()) {
stack_->setCurrentWidget(sessionView_->parentWidget());
}
sessionOverlay_->setText(QStringLiteral("正在连接 %1 …").arg(ticket.endpoint));
sessionOverlay_->show();
MoonlightSession::Options opts;
opts.decoder = QStringLiteral("software"); // RK3528 无可用 V4L2 硬解设备
@@ -454,7 +297,8 @@ void MainWindow::startMoonlightSession(const ConnectionTicket& ticket) {
void MainWindow::onMoonlightState(bool running, const QString& message) {
if (running) {
// 进程已拉起;串流窗口是否成功由 moonlight 自身展示
// 进程已拉起:转入会话态;过场层等首帧检测淡出
fsm_.transitionTo(State::kSession, message);
updateStatusBar(QStringLiteral("SESSION %1").arg(message));
return;
}
@@ -470,59 +314,31 @@ void MainWindow::onMoonlightState(bool running, const QString& message) {
}
}
void MainWindow::onSessionState(bool connected, const QString& message) {
if (connected) {
reconnectAttempts_ = 0;
fsm_.transitionTo(State::kSession, message);
} else {
if (fsm_.current() == State::kConnecting) {
// 首次连接失败:回桌面列表并提示
stopSessions();
fsm_.transitionTo(State::kDesktopList, message);
if (desktopListPage_) {
desktopListPage_->setBusy(false);
desktopListPage_->setStatus(QStringLiteral("连接失败:%1").arg(message), true);
}
} else if (fsm_.current() == State::kSession) {
fsm_.transitionTo(State::kReconnect, message);
void MainWindow::watchFirstFrame() {
if (!moonlightSession_ || !moonlightSession_->isRunning()) {
return; // 进程退出路径由 onMoonlightState 收尾
}
qint64 cpuTicks = -1;
// /proc/<pid>/stat 的 comm 字段可含空格,从最后一个 ')' 后取字段:
// 第 0 个是 state(整行第 3 字段),utime/stime 是第 11/12 个。
QFile statFile(QStringLiteral("/proc/%1/stat").arg(moonlightSession_->processId()));
if (statFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
const QString content = QString::fromLatin1(statFile.readAll());
const QStringList fields =
content.mid(content.lastIndexOf(')') + 1).split(' ', Qt::SkipEmptyParts);
if (fields.size() > 12) {
cpuTicks = fields.value(11).toLongLong() + fields.value(12).toLongLong();
}
}
}
void MainWindow::onFrameWakeup() {
QMutexLocker locker(&frameMutex_);
frameWakePending_ = false;
}
void MainWindow::onFrameRefresh() {
QImage image;
uint64_t epoch = 0;
{
QMutexLocker locker(&frameMutex_);
image = latestFrame_;
epoch = frameEpoch_;
latestFrame_ = QImage();
frameWakePending_ = false;
}
if (epoch == sessionEpoch_) {
onFrameImage(image);
if (cpuTicks >= kFirstFrameCpuTicks || firstFrameClock_.elapsed() >= kFirstFrameTimeoutMs) {
onFirstFrameDetected();
}
}
void MainWindow::onFrameImage(const QImage& image) {
if (image.isNull()) {
return;
}
lastFrame_ = image;
if (sessionView_) {
const QSize targetSize = sessionView_->size();
if (lastFrame_.size() == targetSize) {
sessionView_->setPixmap(QPixmap::fromImage(lastFrame_));
} else {
sessionView_->setPixmap(QPixmap::fromImage(lastFrame_).scaled(
targetSize, Qt::KeepAspectRatio, Qt::FastTransformation));
}
void MainWindow::onFirstFrameDetected() {
firstFrameTimer_->stop();
if (overlay_) {
overlay_->onFirstFrame();
}
}
@@ -532,31 +348,24 @@ void MainWindow::onQuitRequested() {
}
void MainWindow::stopSessions() {
if (rdpSession_) {
rdpSession_->stop();
rdpSession_.reset();
if (firstFrameTimer_) {
firstFrameTimer_->stop();
}
if (moonlightSession_) {
moonlightSession_->stop();
moonlightSession_.reset();
}
QMutexLocker locker(&frameMutex_);
latestFrame_ = QImage();
lastFrame_ = QImage();
frameWakePending_ = false;
if (overlay_) {
overlay_->finish();
}
}
void MainWindow::onMenuAction(int action) {
switch (action) {
case 1: // 重新连接当前桌面
if (fsm_.canConnect() && !activeTicket_.desktopId.isEmpty()) {
reconnectAttempts_ = 0;
stopSessions();
if (activeTicket_.protocol == Protocol::kMoonlight) {
startMoonlightSession(activeTicket_);
} else {
startRdpSession(activeTicket_);
}
startMoonlightSession(activeTicket_);
}
break;
case 2: // 断开回桌面列表
@@ -565,7 +374,7 @@ void MainWindow::onMenuAction(int action) {
fsm_.transitionTo(State::kDesktopList, "已断开");
}
break;
case 3: // 退出终端
case 3: // 退出终端(维护功能,只保留在管理菜单)
onQuitRequested();
break;
default: {
@@ -588,90 +397,9 @@ void MainWindow::onMenuAction(int action) {
} else if (chosen == quit) {
onMenuAction(3);
}
if (stack_->currentWidget() == sessionView_->parentWidget()) {
sessionView_->setFocus(Qt::OtherFocusReason);
}
break;
}
}
}
bool MainWindow::eventFilter(QObject* watched, QEvent* event) {
if (watched == sessionView_ && rdpSession_) {
if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
auto* keyEvent = static_cast<QKeyEvent*>(event);
return forwardKeyEvent(keyEvent, event->type() == QEvent::KeyPress);
}
if (event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonPress ||
event->type() == QEvent::MouseButtonRelease) {
return forwardMouseEvent(static_cast<QMouseEvent*>(event));
}
if (event->type() == QEvent::Wheel) {
return forwardMouseEvent(event);
}
}
return QMainWindow::eventFilter(watched, event);
}
bool MainWindow::forwardKeyEvent(QKeyEvent* event, bool pressed) {
if (!rdpSession_) {
return false;
}
const DWORD rdpScanCode =
freerdp_keyboard_get_rdp_scancode_from_x11_keycode(event->nativeScanCode());
if (rdpScanCode == RDP_SCANCODE_UNKNOWN) {
return false;
}
return rdpSession_->sendKeyEvent(pressed, event->isAutoRepeat(), rdpScanCode);
}
bool MainWindow::forwardMouseEvent(QEvent* event) {
if (!rdpSession_) {
return false;
}
uint16_t flags = 0;
QPoint pos = lastMousePosition_;
if (event->type() == QEvent::MouseMove) {
auto* mouse = static_cast<QMouseEvent*>(event);
pos = mouse->pos();
flags = PTR_FLAGS_MOVE;
} else if (event->type() == QEvent::MouseButtonPress ||
event->type() == QEvent::MouseButtonRelease) {
auto* mouse = static_cast<QMouseEvent*>(event);
pos = mouse->pos();
if (mouse->button() == Qt::LeftButton) flags |= PTR_FLAGS_BUTTON1;
if (mouse->button() == Qt::RightButton) flags |= PTR_FLAGS_BUTTON2;
if (mouse->button() == Qt::MiddleButton) flags |= PTR_FLAGS_BUTTON3;
if (event->type() == QEvent::MouseButtonPress) flags |= PTR_FLAGS_DOWN;
} else if (event->type() == QEvent::Wheel) {
auto* wheel = static_cast<QWheelEvent*>(event);
pos = wheel->position().toPoint();
flags = PTR_FLAGS_WHEEL;
if (wheel->angleDelta().y() < 0) flags |= PTR_FLAGS_WHEEL_NEGATIVE;
}
lastMousePosition_ = pos;
if (lastFrame_.isNull() || !sessionView_) {
return false;
}
const QSize sourceSize = lastFrame_.size();
const QSize viewSize = sessionView_->size();
const QSize displayedSize = sourceSize.scaled(viewSize, Qt::KeepAspectRatio);
const int offsetX = (viewSize.width() - displayedSize.width()) / 2;
const int offsetY = (viewSize.height() - displayedSize.height()) / 2;
if (pos.x() < offsetX || pos.y() < offsetY ||
pos.x() >= offsetX + displayedSize.width() ||
pos.y() >= offsetY + displayedSize.height()) {
return true;
}
const int remoteX = qBound(0,
(pos.x() - offsetX) * sourceSize.width() / displayedSize.width(),
sourceSize.width() - 1);
const int remoteY = qBound(0,
(pos.y() - offsetY) * sourceSize.height() / displayedSize.height(),
sourceSize.height() - 1);
return rdpSession_->sendMouseEvent(flags, static_cast<uint16_t>(remoteX),
static_cast<uint16_t>(remoteY));
}
} // namespace vdi