功能:实现桌面选择流程并接入 Moonlight 会话
变更详情:feat(kiosk-shell): 登录/桌面列表产品流程 + Moonlight 会话 + 看门狗进程组修复 - 登录页改为用户认证(本地演示认证,账号密码非空通过) - 新增桌面列表页:Windows/Moonlight 可连接,Ubuntu/麒麟待分配 - 新增 DesktopService 适配层(登录/桌面列表/连接票据,后续接 API) - 新增 MoonlightSession:QProcess 参数数组直启 moonlight-qt,退出回列表 - 状态机扩展 AUTHENTICATING / DESKTOP_LIST - 移除开机自动 RDP,收敛为 monitor -> kiosk-shell 单一自启链 - Monitor 按进程组结束整棵进程树,修复孤儿串流进程 - vdi-moonlight 启动器新增 vsync/frame-pacing/overlay 低延迟开关
This commit is contained in:
1 parent
f74d3ceadf
commit
61127fb714
25 files changed
+1982
-246
No files matched your search
@@ -1,12 +1,16 @@
|
||||
#include "main_window.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QCloseEvent>
|
||||
#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>
|
||||
@@ -14,8 +18,12 @@
|
||||
#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 "login_page.h"
|
||||
|
||||
namespace vdi {
|
||||
@@ -31,15 +39,21 @@ QImage::Format toQImageFormat(uint32_t freerdpFormat) {
|
||||
return QImage::Format_RGB888;
|
||||
}
|
||||
|
||||
// 把 FreeRDP 的 4 字节/像素缓冲转成 QImage 可用的 RGB888
|
||||
// bytesPerLine 是 FreeRDP 的每行字节数(width*4)
|
||||
// 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();
|
||||
}
|
||||
const bool bgr = (freerdpFormat == PIXEL_FORMAT_BGRX32) ||
|
||||
(freerdpFormat == PIXEL_FORMAT_BGRA32);
|
||||
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) {
|
||||
@@ -47,15 +61,9 @@ QImage convertFrame(const uint8_t* data, uint32_t width, uint32_t height, uint32
|
||||
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;
|
||||
if (bgr) {
|
||||
dst[0] = p[2];
|
||||
dst[1] = p[1];
|
||||
dst[2] = p[0];
|
||||
} else {
|
||||
dst[0] = p[0];
|
||||
dst[1] = p[1];
|
||||
dst[2] = p[2];
|
||||
}
|
||||
dst[0] = p[0];
|
||||
dst[1] = p[1];
|
||||
dst[2] = p[2];
|
||||
dst += 3;
|
||||
}
|
||||
}
|
||||
@@ -73,15 +81,19 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
});
|
||||
setupUi();
|
||||
setupHotkey();
|
||||
setupHeartbeat();
|
||||
keyboardLayout_ = freerdp_keyboard_init(0);
|
||||
|
||||
// BOOT -> CONFIG
|
||||
config_ = loadConfig();
|
||||
fsm_.transitionTo(State::kConfig, "启动完成");
|
||||
|
||||
// 开机永远进入登录页;不再根据本地配置自动连接 RDP,
|
||||
// 会话只能由用户在桌面列表选择后启动。
|
||||
fsm_.transitionTo(State::kConfig, QStringLiteral("等待登录"));
|
||||
showLogin();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() {
|
||||
stopSession();
|
||||
stopSessions();
|
||||
}
|
||||
|
||||
void MainWindow::setupUi() {
|
||||
@@ -94,7 +106,6 @@ void MainWindow::setupUi() {
|
||||
setCentralWidget(stack_);
|
||||
|
||||
// 登录页
|
||||
config_ = loadConfig();
|
||||
auto* login = new LoginPage(config_, this);
|
||||
loginPage_ = login;
|
||||
login->setStyleSheet(
|
||||
@@ -102,23 +113,36 @@ void MainWindow::setupUi() {
|
||||
"QLineEdit { background: #1a2233; border: 1px solid #2b3654; "
|
||||
"border-radius: 4px; padding: 8px; color: #e6e9ef; }"
|
||||
"QLineEdit:focus { border: 1px solid #2f6feb; }"
|
||||
"QComboBox { background: #1a2233; border: 1px solid #2b3654; "
|
||||
"border-radius: 4px; padding: 8px; color: #e6e9ef; }"
|
||||
"QPushButton { background: #232d44; border: 1px solid #2b3654; "
|
||||
"border-radius: 6px; color: #e6e9ef; }"
|
||||
"QPushButton:hover { background: #2b3654; }"
|
||||
"QCheckBox { color: #8a94a6; }"));
|
||||
"QPushButton:hover { background: #2b3654; }"));
|
||||
stack_->addWidget(login);
|
||||
|
||||
connect(login, &LoginPage::connectRequested, this, &MainWindow::onConnectRequested);
|
||||
connect(login, &LoginPage::loginRequested, this, &MainWindow::onLoginRequested);
|
||||
connect(login, &LoginPage::quitRequested, this, &MainWindow::onQuitRequested);
|
||||
|
||||
// 桌面列表页
|
||||
auto* list = new DesktopListPage(this);
|
||||
desktopListPage_ = list;
|
||||
list->setStyleSheet(
|
||||
QStringLiteral("QWidget { background: #0f1420; color: #e6e9ef; }"
|
||||
"QPushButton { background: #232d44; border: 1px solid #2b3654; "
|
||||
"border-radius: 6px; color: #e6e9ef; }"
|
||||
"QPushButton:hover { background: #2b3654; }"));
|
||||
stack_->addWidget(list);
|
||||
|
||||
connect(list, &DesktopListPage::desktopSelected, this, &MainWindow::onDesktopSelected);
|
||||
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("正在建立远程会话…"));
|
||||
@@ -140,17 +164,27 @@ void MainWindow::setupUi() {
|
||||
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_;
|
||||
startSession(config_);
|
||||
if (activeTicket_.protocol == Protocol::kMoonlight) {
|
||||
startMoonlightSession(activeTicket_);
|
||||
} else {
|
||||
startRdpSession(activeTicket_);
|
||||
}
|
||||
} else if (fsm_.current() == State::kReconnect) {
|
||||
fsm_.transitionTo(State::kError, "重连失败,请检查网络");
|
||||
stopSessions();
|
||||
fsm_.transitionTo(State::kDesktopList, QStringLiteral("重连失败,已返回桌面列表"));
|
||||
}
|
||||
});
|
||||
|
||||
frameRefreshTimer_ = new QTimer(this);
|
||||
frameRefreshTimer_->setInterval(33);
|
||||
connect(frameRefreshTimer_, &QTimer::timeout, this, &MainWindow::onFrameRefresh);
|
||||
frameRefreshTimer_->start();
|
||||
}
|
||||
|
||||
void MainWindow::setupHotkey() {
|
||||
@@ -160,6 +194,22 @@ void MainWindow::setupHotkey() {
|
||||
connect(shortcut, &QShortcut::activated, this, [this]() { onMenuAction(0); });
|
||||
}
|
||||
|
||||
void MainWindow::setupHeartbeat() {
|
||||
// 定期写心跳文件,供看门狗 Monitor 判断主进程是否假死
|
||||
// 对应会议纪要要求的双进程架构
|
||||
const QString path = QStringLiteral("/tmp/vdi-kiosk.heartbeat");
|
||||
heartbeatTimer_ = new QTimer(this);
|
||||
heartbeatTimer_->setInterval(5000);
|
||||
connect(heartbeatTimer_, &QTimer::timeout, this, [path]() {
|
||||
QFile file(path);
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
file.write(QByteArray::number(QDateTime::currentSecsSinceEpoch()));
|
||||
file.close();
|
||||
}
|
||||
});
|
||||
heartbeatTimer_->start();
|
||||
}
|
||||
|
||||
void MainWindow::onStateChanged(State from, State to, const QString& detail) {
|
||||
Q_UNUSED(from);
|
||||
const QString stateName = QString::fromLatin1(stateToString(to));
|
||||
@@ -169,15 +219,22 @@ void MainWindow::onStateChanged(State from, State to, const QString& detail) {
|
||||
case State::kConfig:
|
||||
showLogin();
|
||||
break;
|
||||
case State::kAuthenticating:
|
||||
break;
|
||||
case State::kDesktopList:
|
||||
showDesktopList();
|
||||
refreshDesktopList();
|
||||
break;
|
||||
case State::kConnecting:
|
||||
if (stack_->currentWidget() != sessionView_->parentWidget()) {
|
||||
stack_->setCurrentWidget(sessionView_->parentWidget());
|
||||
}
|
||||
sessionOverlay_->setText(QStringLiteral("正在连接 %1 …")
|
||||
.arg(QString::fromStdString(config_.server)));
|
||||
sessionView_->setFocus(Qt::OtherFocusReason);
|
||||
sessionOverlay_->setText(QStringLiteral("正在连接 %1 …").arg(activeTicket_.endpoint));
|
||||
sessionOverlay_->show();
|
||||
break;
|
||||
case State::kSession:
|
||||
sessionView_->setFocus(Qt::OtherFocusReason);
|
||||
sessionOverlay_->hide();
|
||||
break;
|
||||
case State::kReconnect:
|
||||
@@ -186,11 +243,11 @@ void MainWindow::onStateChanged(State from, State to, const QString& detail) {
|
||||
reconnectTimer_->start(kReconnectDelayMs);
|
||||
break;
|
||||
case State::kError:
|
||||
sessionOverlay_->setText(QStringLiteral("连接失败:%1").arg(detail));
|
||||
sessionOverlay_->show();
|
||||
if (loginPage_) {
|
||||
loginPage_->setBusy(false);
|
||||
loginPage_->setStatus(QStringLiteral("连接失败:%1").arg(detail), true);
|
||||
stopSessions();
|
||||
fsm_.transitionTo(State::kDesktopList, QStringLiteral("连接失败"));
|
||||
if (desktopListPage_) {
|
||||
desktopListPage_->setBusy(false);
|
||||
desktopListPage_->setStatus(QStringLiteral("连接失败:%1").arg(detail), true);
|
||||
}
|
||||
break;
|
||||
case State::kBoot:
|
||||
@@ -206,38 +263,104 @@ void MainWindow::showLogin() {
|
||||
stack_->setCurrentWidget(loginPage_);
|
||||
if (loginPage_) {
|
||||
loginPage_->setBusy(false);
|
||||
loginPage_->setStatus(QStringLiteral("就绪"));
|
||||
loginPage_->setStatus(QStringLiteral("请输入用户名和密码登录"));
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onConnectRequested(const vdi::TerminalConfig& cfg) {
|
||||
config_ = cfg;
|
||||
reconnectAttempts_ = 0;
|
||||
startSession(cfg);
|
||||
void MainWindow::showDesktopList() {
|
||||
if (!desktopListPage_) {
|
||||
return;
|
||||
}
|
||||
stack_->setCurrentWidget(desktopListPage_);
|
||||
desktopListPage_->setCurrentUser(currentUser_);
|
||||
desktopListPage_->setBusy(false);
|
||||
}
|
||||
|
||||
void MainWindow::startSession(const TerminalConfig& cfg) {
|
||||
if (!fsm_.transitionTo(State::kConnecting, QString::fromStdString(cfg.server))) {
|
||||
// 已经在 CONNECTING 或 SESSION,忽略重复触发
|
||||
void MainWindow::refreshDesktopList() {
|
||||
desktopService_.listDesktops([this](std::vector<DesktopInfo> list, const QString& error) {
|
||||
if (error.isEmpty()) {
|
||||
desktops_ = list;
|
||||
}
|
||||
if (desktopListPage_) {
|
||||
desktopListPage_->setDesktops(desktops_);
|
||||
if (!error.isEmpty()) {
|
||||
desktopListPage_->setStatus(error, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::onLoginRequested(const QString& username, const QString& password) {
|
||||
desktopService_.login(username, password, [this](const AuthResult& result) {
|
||||
if (!loginPage_) {
|
||||
return;
|
||||
}
|
||||
if (!result.ok) {
|
||||
loginPage_->setBusy(false);
|
||||
loginPage_->setStatus(result.message, true);
|
||||
return;
|
||||
}
|
||||
currentUser_ = result.displayName;
|
||||
fsm_.transitionTo(State::kAuthenticating, QStringLiteral("认证成功"));
|
||||
fsm_.transitionTo(State::kDesktopList, QStringLiteral("欢迎 %1").arg(currentUser_));
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::onDesktopSelected(const QString& desktopId) {
|
||||
if (!fsm_.canConnect()) {
|
||||
return;
|
||||
}
|
||||
desktopService_.createTicket(desktopId, [this](const ConnectionTicket& ticket,
|
||||
const QString& error) {
|
||||
if (!error.isEmpty()) {
|
||||
if (desktopListPage_) {
|
||||
desktopListPage_->setBusy(false);
|
||||
desktopListPage_->setStatus(error, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
activeTicket_ = ticket;
|
||||
reconnectAttempts_ = 0;
|
||||
|
||||
if (ticket.protocol == Protocol::kMoonlight) {
|
||||
startMoonlightSession(ticket);
|
||||
} else {
|
||||
startRdpSession(ticket);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::onLogoutRequested() {
|
||||
stopSessions();
|
||||
desktopService_.logout();
|
||||
currentUser_.clear();
|
||||
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 = cfg.server;
|
||||
opts.username = cfg.username;
|
||||
opts.password = cfg.password;
|
||||
|
||||
// 解析分辨率
|
||||
int w = 1920;
|
||||
int h = 1440;
|
||||
if (sscanf(cfg.resolution.c_str(), "%dx%d", &w, &h) != 2) {
|
||||
w = 1920;
|
||||
h = 1440;
|
||||
}
|
||||
opts.width = w;
|
||||
opts.height = h;
|
||||
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 能力组合语义:
|
||||
@@ -248,12 +371,20 @@ void MainWindow::startSession(const TerminalConfig& cfg) {
|
||||
RdpCallbacks cb;
|
||||
cb.onFrame = [this](const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride,
|
||||
uint32_t format) {
|
||||
// 帧数据来自 FreeRDP 线程,切换到 GUI 线程更新
|
||||
QMetaObject::invokeMethod(
|
||||
this, [this, data, width, height, stride, format]() {
|
||||
onFrame(data, width, height, stride, format);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
// 在 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(
|
||||
@@ -264,15 +395,54 @@ void MainWindow::startSession(const TerminalConfig& cfg) {
|
||||
Qt::QueuedConnection);
|
||||
};
|
||||
|
||||
stopSession();
|
||||
session_ = std::make_unique<RdpSession>();
|
||||
session_->start(opts, cb);
|
||||
rdpSession_ = std::make_unique<RdpSession>();
|
||||
rdpSession_->start(opts, cb);
|
||||
}
|
||||
|
||||
void MainWindow::stopSession() {
|
||||
if (session_) {
|
||||
session_->stop();
|
||||
session_.reset();
|
||||
void MainWindow::startMoonlightSession(const ConnectionTicket& ticket) {
|
||||
if (!fsm_.transitionTo(State::kConnecting, ticket.endpoint)) {
|
||||
if (fsm_.current() != State::kConnecting) {
|
||||
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 硬解设备
|
||||
opts.videoCodec = QStringLiteral("H.264");
|
||||
opts.audioOnHost = true;
|
||||
|
||||
moonlightSession_ = std::make_unique<MoonlightSession>(/*parent=*/nullptr);
|
||||
moonlightSession_->setOptions(opts);
|
||||
moonlightSession_->start(ticket, [this](bool running, const QString& message) {
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this, running, message]() { onMoonlightState(running, message); },
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::onMoonlightState(bool running, const QString& message) {
|
||||
if (running) {
|
||||
// 进程已拉起;串流窗口是否成功由 moonlight 自身展示
|
||||
updateStatusBar(QStringLiteral("SESSION %1").arg(message));
|
||||
return;
|
||||
}
|
||||
// 进程退出:回桌面列表
|
||||
moonlightSession_.reset();
|
||||
stopSessions();
|
||||
if (fsm_.current() == State::kConnecting || fsm_.current() == State::kSession) {
|
||||
fsm_.transitionTo(State::kDesktopList, message);
|
||||
}
|
||||
if (desktopListPage_) {
|
||||
desktopListPage_->setBusy(false);
|
||||
desktopListPage_->setStatus(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,56 +452,102 @@ void MainWindow::onSessionState(bool connected, const QString& message) {
|
||||
fsm_.transitionTo(State::kSession, message);
|
||||
} else {
|
||||
if (fsm_.current() == State::kConnecting) {
|
||||
// 首次连接失败
|
||||
fsm_.transitionTo(State::kError, message);
|
||||
// 首次连接失败:回桌面列表并提示
|
||||
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::onFrame(const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride,
|
||||
uint32_t format) {
|
||||
if (!data || width == 0 || height == 0) {
|
||||
return;
|
||||
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;
|
||||
}
|
||||
// FreeRDP 主缓冲是 4 字节/像素,需转换后交给 Qt
|
||||
QImage image = convertFrame(data, width, height, stride, format);
|
||||
if (epoch == sessionEpoch_) {
|
||||
onFrameImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onFrameImage(const QImage& image) {
|
||||
if (image.isNull()) {
|
||||
return;
|
||||
}
|
||||
lastFrame_ = image;
|
||||
frameDirty_ = true;
|
||||
|
||||
if (sessionView_) {
|
||||
sessionView_->setPixmap(QPixmap::fromImage(lastFrame_).scaled(
|
||||
sessionView_->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
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::onQuitRequested() {
|
||||
stopSession();
|
||||
stopSessions();
|
||||
QApplication::quit();
|
||||
}
|
||||
|
||||
void MainWindow::stopSessions() {
|
||||
if (rdpSession_) {
|
||||
rdpSession_->stop();
|
||||
rdpSession_.reset();
|
||||
}
|
||||
if (moonlightSession_) {
|
||||
moonlightSession_->stop();
|
||||
moonlightSession_.reset();
|
||||
}
|
||||
QMutexLocker locker(&frameMutex_);
|
||||
latestFrame_ = QImage();
|
||||
lastFrame_ = QImage();
|
||||
frameWakePending_ = false;
|
||||
}
|
||||
|
||||
void MainWindow::onMenuAction(int action) {
|
||||
switch (action) {
|
||||
case 1: // 重新连接
|
||||
reconnectAttempts_ = 0;
|
||||
stopSession();
|
||||
startSession(config_);
|
||||
case 1: // 重新连接当前桌面
|
||||
if (fsm_.canConnect() && !activeTicket_.desktopId.isEmpty()) {
|
||||
reconnectAttempts_ = 0;
|
||||
stopSessions();
|
||||
if (activeTicket_.protocol == Protocol::kMoonlight) {
|
||||
startMoonlightSession(activeTicket_);
|
||||
} else {
|
||||
startRdpSession(activeTicket_);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2: // 断开回登录页
|
||||
stopSession();
|
||||
fsm_.transitionTo(State::kConfig, "已断开");
|
||||
case 2: // 断开回桌面列表
|
||||
stopSessions();
|
||||
if (fsm_.current() != State::kConfig) {
|
||||
fsm_.transitionTo(State::kDesktopList, "已断开");
|
||||
}
|
||||
break;
|
||||
case 3: // 退出终端
|
||||
onQuitRequested();
|
||||
break;
|
||||
default: {
|
||||
QMenu menu(this);
|
||||
QAction* reconnect = menu.addAction(QStringLiteral("重新连接"));
|
||||
QAction* disconnect = menu.addAction(QStringLiteral("断开并返回登录页"));
|
||||
QAction* reconnect = menu.addAction(QStringLiteral("重新连接当前桌面"));
|
||||
QAction* disconnect = menu.addAction(QStringLiteral("断开并返回桌面列表"));
|
||||
menu.addSeparator();
|
||||
QAction* quit = menu.addAction(QStringLiteral("退出终端"));
|
||||
QAction* chosen = menu.exec(QCursor::pos());
|
||||
@@ -342,13 +558,90 @@ 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
|
||||
Reference in new issue
Block a user