feat(kiosk-shell): FreeRDP 3 内嵌客户端跑通,登录页可进入远程桌面

- 按方案 3.2/3.3 节以 libfreerdp3 API 内嵌,不拉起外部进程
- gdi_init 移到 PostConnect(提前调用会段错误)
- 去掉 ContextSize 扩展 rdpContext,改用 freerdp* 注册表 + gfx->custom
- NLA 认证传空字符串而非 nullptr,修复 SEC_E_NO_CREDENTIALS
- 补充 FreeRDP3 内嵌实现笔记文档
This commit is contained in:
dev committed 2026-10-23 18:57:05 +08:00
1 parent 419630fcd7
commit 589cde457c
8 files changed
+366 -152

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
# FreeRDP 3 内嵌客户端实现笔记(Ubuntu 24.04 / freerdp3-dev 3.32.1)
> 对应《VDI 云桌面软件开发方案》3.2 / 3.3 节:以 libfreerdp3 API 内嵌(非拉起外部进程)。
> 记录时间:2026-10-23,实测于 RK3528 盒子(Armbian / Ubuntu Noble / aarch64)
## 1. API 结构(与 FreeRDP 2.x 差异很大)
| 用途 | FreeRDP 3 写法 |
|---|---|
| 创建实例 | `freerdp* instance = freerdp_new();` |
| 建立上下文 | `freerdp_context_new_ex(instance, settings)` |
| 上下文 | `instance->context`(`rdpContext*`,公开结构体) |
| 事件回调 | `instance->PostConnect` / `instance->PostDisconnect` / `instance->AuthenticateEx` |
| 更新回调 | `instance->context->update->BitmapUpdate` / `->EndPaint` |
| GDI | `gdi_init(instance, PIXEL_FORMAT_BGRX32)` |
| 取帧 | `context->gdi->primary_buffer` + `context->gdi->stride` |
设置项键名(与 2.x 不同):
`FreeRDP_ServerHostname` / `FreeRDP_Username` / `FreeRDP_Password` / `FreeRDP_Domain` /
`FreeRDP_DesktopWidth` / `FreeRDP_DesktopHeight` / `FreeRDP_ColorDepth` / `FreeRDP_Fullscreen` /
`FreeRDP_IgnoreCertificate` / `FreeRDP_GfxProgressive` / `FreeRDP_GfxH264` / `FreeRDP_GfxAVC444` …
## 2. 三个必须踩过才知道的坑
### 坑一:gdi_init 必须在 PostConnect 里调用
在 `freerdp_context_new_ex()` 之后立刻调用 `gdi_init()` 会 **SIGSEGV**
(`gdi_init_ex` 段错误)。官方 x11 客户端(`client/X11/xf_client.c` 的 `xf_post_connect`)
把 `gdi_init` 放在 PostConnect 回调内,因为 GDI 依赖连接协商后的桌面尺寸与颜色格式。
### 坑二:不能用 instance->ContextSize 扩展 rdpContext
常见做法是把自定义指针塞进扩展的 rdpContext,但 `gdi_init_ex()` 会按 `ContextSize`
重新分配 context 缓冲区,扩展字段随即失效 → 段错误。
改用静态注册表按 `freerdp*` 记录会话对象;GFX 通道回调则用官方的
`RdpgfxClientContext.custom` 字段。
### 坑三:NLA 认证必须传空字符串而非 nullptr
Windows 端启用 NLA(CredSSP)时,认证回调里把密码置为 `nullptr` 会导致
`SEC_E_NO_CREDENTIALS [0x8009030e]`。必须传 `strdup("")`。
认证回调签名:
`BOOL (*pAuthenticateEx)(freerdp*, char** username, char** password, char** domain, rdp_auth_reason)`
## 3. /gfx 语法(命令行与设置项的对应)
FreeRDP 3.32.1 的 `/gfx` **不接受单个管道名**,必须是 `|` 分隔的能力组合:
```
/gfx:progressive|RFX|AVC420|AVC444 ← 可解析
/gfx:AVC420 ← parsing failed [-1000]
/gfx:AVC444 ← parsing failed [-1000]
```
API 侧对应 `FreeRDP_GfxProgressive` / `FreeRDP_GfxH264` / `FreeRDP_GfxAVC444` 等布尔项。
## 4. 像素格式
`PIXEL_FORMAT_BGRX32` 的内存布局是 **B,G,R,X** 四字节/像素。
QImage 没有对应格式,输出前需手动做 BGR→RGB 转换。
注意 `PIXEL_FORMAT_*` 是计算宏 `FREERDP_PIXEL_FORMAT(...)`,**不能硬编码数值**。
## 5. 后续演进(对应 freerdp-rk 分支)
当前实现是 CPU 侧 GDI 缓冲 → Qt 绘制。开发方案 3.3 节要求改为:
```text
FreeRDP3 解复用(gfx 通道) → GStreamer h264parse → mppvideodec(VPU硬解, zero-copy DMABuf)
→ waylandsink(DRM plane)
```
盒子已具备 `/dev/mpp_service`(VPU)与 `/dev/rga`,但缺 `mppvideodec` GStreamer 插件
(Ubuntu 官方源没有,需 Rockchip BSP / Armbian 额外源或自研)。
+15 -16
View File
@@ -7,6 +7,7 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QProcess>
#include <QPushButton> #include <QPushButton>
#include <QVBoxLayout> #include <QVBoxLayout>
@@ -16,22 +17,26 @@ namespace {
const char* kResolutionIds[] = {"1920x1440", "2560x1440", "1920x1080", "1280x720"}; const char* kResolutionIds[] = {"1920x1440", "2560x1440", "1920x1080", "1280x720"};
QString detectNetworkSummary() { QString detectNetworkSummary() {
// 简单读取默认路由与第一块非回环网卡地址 // 取默认路由的出接口与该接口上的 IPv4 地址
QProcess process; QProcess process;
process.start("sh", QStringList() << "-c" << process.start("sh", QStringList() << "-c" <<
"ip -4 route show default | awk '{print $NF}'; " "dev=$(ip -4 route show default | awk '{print $NF}' | head -n 1); "
"ip -4 -o addr show scope global | awk '{print $4}' | head -n 1"); "addr=$(ip -4 -o addr show dev \"$dev\" | awk '{print $4}' | head -n 1); "
"echo \"$dev $addr\"");
process.waitForFinished(2000); process.waitForFinished(2000);
const QString out = QString::fromUtf8(process.readAllStandardOutput()).trimmed(); const QString out = QString::fromUtf8(process.readAllStandardOutput()).trimmed();
if (out.isEmpty()) { if (out.isEmpty()) {
return QStringLiteral("网络:未连接"); return QStringLiteral("网络:未连接");
} }
QStringList lines = out.split('\n', Qt::SkipEmptyParts); const QStringList parts = out.split(' ', Qt::SkipEmptyParts);
QString iface = lines.value(0); QString iface = parts.value(0);
QString addr = lines.value(1); QString addr = parts.value(1);
if (addr.contains('/')) { if (addr.contains('/')) {
addr = addr.split('/').value(0); addr = addr.split('/').value(0);
} }
if (iface.isEmpty()) {
return QStringLiteral("网络:未连接");
}
return QStringLiteral("网络:%1 (%2)").arg(iface, addr); return QStringLiteral("网络:%1 (%2)").arg(iface, addr);
} }
@@ -62,7 +67,7 @@ LoginPage::LoginPage(const TerminalConfig& initial, QWidget* parent)
// ---- 设备信息条 ---- // ---- 设备信息条 ----
auto* infoBox = new QHBoxLayout(); auto* infoBox = new QHBoxLayout();
labelSn_ = new QLabel(QStringLiteral("设备 SN:%1").arg(cfg_.sn), this); labelSn_ = new QLabel(QStringLiteral("设备 SN:%1").arg(QString::fromStdString(cfg_.sn)), this);
labelNetwork_ = new QLabel(detectNetworkSummary(), this); labelNetwork_ = new QLabel(detectNetworkSummary(), this);
labelSn_->setStyleSheet(QStringLiteral("color: #8a94a6;")); labelSn_->setStyleSheet(QStringLiteral("color: #8a94a6;"));
labelNetwork_->setStyleSheet(QStringLiteral("color: #8a94a6;")); labelNetwork_->setStyleSheet(QStringLiteral("color: #8a94a6;"));
@@ -77,11 +82,11 @@ LoginPage::LoginPage(const TerminalConfig& initial, QWidget* parent)
grid->setVerticalSpacing(12); grid->setVerticalSpacing(12);
grid->setColumnStretch(1, 1); grid->setColumnStretch(1, 1);
editServer_ = new QLineEdit(cfg_.server, this); editServer_ = new QLineEdit(QString::fromStdString(cfg_.server), this);
editServer_->setPlaceholderText(QStringLiteral("服务器地址,例如 192.168.1.70")); editServer_->setPlaceholderText(QStringLiteral("服务器地址,例如 192.168.1.70"));
editUser_ = new QLineEdit(cfg_.username, this); editUser_ = new QLineEdit(QString::fromStdString(cfg_.username), this);
editUser_->setPlaceholderText(QStringLiteral("登录账号")); editUser_->setPlaceholderText(QStringLiteral("登录账号"));
editPassword_ = new QLineEdit(cfg_.password, this); editPassword_ = new QLineEdit(QString::fromStdString(cfg_.password), this);
editPassword_->setEchoMode(QLineEdit::Password); editPassword_->setEchoMode(QLineEdit::Password);
editPassword_->setPlaceholderText(QStringLiteral("登录密码(可留空)")); editPassword_->setPlaceholderText(QStringLiteral("登录密码(可留空)"));
@@ -143,8 +148,6 @@ LoginPage::LoginPage(const TerminalConfig& initial, QWidget* parent)
connect(buttonConnect_, &QPushButton::clicked, this, &LoginPage::onConnectClicked); connect(buttonConnect_, &QPushButton::clicked, this, &LoginPage::onConnectClicked);
connect(buttonQuit_, &QPushButton::clicked, this, [this]() { emit quitRequested(); }); connect(buttonQuit_, &QPushButton::clicked, this, [this]() { emit quitRequested(); });
connect(comboResolution_, QOverload<int>::of(&LoginPage::onResolutionChanged), this,
&LoginPage::onResolutionChanged);
} }
void LoginPage::setConnectHandler(ConnectHandler handler) { void LoginPage::setConnectHandler(ConnectHandler handler) {
@@ -195,8 +198,4 @@ void LoginPage::onConnectClicked() {
emit connectRequested(cfg); emit connectRequested(cfg);
} }
void LoginPage::onResolutionChanged(int) {
// 分辨率变更即时生效,实际尺寸在建立连接时读取
}
} // namespace vdi } // namespace vdi
-1
View File
@@ -43,7 +43,6 @@ signals:
private slots: private slots:
void onConnectClicked(); void onConnectClicked();
void onResolutionChanged(int index);
private: private:
TerminalConfig cfg_; TerminalConfig cfg_;
+46 -16
View File
@@ -14,23 +14,52 @@
#include <QTimer> #include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <freerdp/codec/color.h>
#include "login_page.h" #include "login_page.h"
namespace vdi { namespace vdi {
namespace { namespace {
// FreeRDP 像素格式 -> QImage::Format // FreeRDP 像素格式 -> QImage::Format
// 注意:FreeRDP 的像素格式值是计算宏(FREERDP_PIXEL_FORMAT),
// 不能硬编码数值,必须用宏直接比较。
// FreeRDP 主缓冲是 4 字节/像素(BGRX / BGRA / RGBA),
// QImage 没有对应的 4 字节 BGRA 格式,因此统一转成 RGB888 输出。
QImage::Format toQImageFormat(uint32_t freerdpFormat) { QImage::Format toQImageFormat(uint32_t freerdpFormat) {
switch (freerdpFormat) { (void)freerdpFormat;
case 0x00000020: // PIXEL_FORMAT_BGRX8888 return QImage::Format_RGB888;
return QImage::Format_RGB888; }
case 0x00000030: // PIXEL_FORMAT_BGRA8888
return QImage::Format_RGBA8888; // 把 FreeRDP 的 4 字节/像素缓冲转成 QImage 可用的 RGB888
case 0x00000000: // PIXEL_FORMAT_RGBX8888 // bytesPerLine 是 FreeRDP 的每行字节数(width*4)
return QImage::Format_RGBX8888; QImage convertFrame(const uint8_t* data, uint32_t width, uint32_t height, uint32_t bytesPerLine,
default: uint32_t freerdpFormat) {
return QImage::Format_RGBA8888; if (!data || width == 0 || height == 0) {
return QImage();
} }
const bool bgr = (freerdpFormat == PIXEL_FORMAT_BGRX32) ||
(freerdpFormat == PIXEL_FORMAT_BGRA32);
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;
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 += 3;
}
}
return out;
} }
constexpr int kReconnectDelayMs = 3000; constexpr int kReconnectDelayMs = 3000;
@@ -39,8 +68,8 @@ constexpr int kMaxReconnectAttempts = 5;
} // namespace } // namespace
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
fsm_ = StateMachine([this](State from, State to, const std::string& detail) { fsm_ = StateMachine([this](State from, State to, const QString& detail) {
onStateChanged(from, to, QString::fromStdString(detail)); onStateChanged(from, to, detail);
}); });
setupUi(); setupUi();
setupHotkey(); setupHotkey();
@@ -266,11 +295,12 @@ void MainWindow::onFrame(const uint8_t* data, uint32_t width, uint32_t height, u
if (!data || width == 0 || height == 0) { if (!data || width == 0 || height == 0) {
return; return;
} }
const QImage::Format fmt = toQImageFormat(format); // FreeRDP 主缓冲是 4 字节/像素,需转换后交给 Qt
// FreeRDP 的 primary_buffer 是 BGRA/BGRX 排列,QImage 需要拷贝 QImage image = convertFrame(data, width, height, stride, format);
QImage image(reinterpret_cast<const uchar*>(data), static_cast<int>(width), if (image.isNull()) {
static_cast<int>(height), static_cast<int>(stride), fmt); return;
lastFrame_ = image.copy(); // 拷贝,脱离 FreeRDP 缓冲生命周期 }
lastFrame_ = image;
frameDirty_ = true; frameDirty_ = true;
if (sessionView_) { if (sessionView_) {
+203 -102
View File
@@ -1,20 +1,50 @@
#include "rdp_session.h" #include "rdp_session.h"
#include <cstddef>
#include <cstring> #include <cstring>
#include <map>
#include <mutex> #include <mutex>
namespace vdi { namespace vdi {
namespace { namespace {
// 会话实例指针表:通过 rdpContext->handle 取回 C++ 对象 // 从 freerdp 实例取回 C++ 会话对象。
struct SessionSlot { // 注意:不能用 instance->ContextSize 扩展 rdpContext ——
RdpSession* session = nullptr; // gdi_init_ex() 会按 ContextSize 重新分配 context 缓冲区,
// 扩展出来的字段会失效(实测段错误)。
// 因此改用 FreeRDP 官方提供的 custom 挂载点:
// rdpGdi->gfx->custom 用于 GFX 通道回调;
// 这里额外用一个静态注册表按 freerdp* 记录会话对象。
class SessionRegistry {
public:
static void add(freerdp* instance, RdpSession* session) {
std::lock_guard<std::mutex> lock(mutex());
table()[instance] = session;
}
static void remove(freerdp* instance) {
std::lock_guard<std::mutex> lock(mutex());
table().erase(instance);
}
static RdpSession* get(freerdp* instance) {
if (!instance) return nullptr;
std::lock_guard<std::mutex> lock(mutex());
auto it = table().find(instance);
return it == table().end() ? nullptr : it->second;
}
private:
static std::mutex& mutex() {
static std::mutex m;
return m;
}
static std::map<freerdp*, RdpSession*>& table() {
static std::map<freerdp*, RdpSession*> t;
return t;
}
}; };
SessionSlot* slotOf(rdpContext* context) { RdpSession* sessionOf(freerdp* instance) {
if (!context) return nullptr; return SessionRegistry::get(instance);
auto* slot = static_cast<SessionSlot*>(context->handle);
return slot;
} }
} // namespace } // namespace
@@ -41,30 +71,20 @@ void RdpSession::stop() {
return; return;
} }
running_.store(false); running_.store(false);
if (context_) { if (instance_) {
// 请求断开;连接线程会在 freerdp_disconnect 后返回 // 请求断开;连接线程会从事件循环返回
freerdp_disconnect(context_); freerdp_disconnect(instance_);
} }
if (worker_.joinable()) { if (worker_.joinable()) {
worker_.join(); worker_.join();
} }
instance_ = nullptr;
context_ = nullptr; context_ = nullptr;
} }
void RdpSession::run() { void RdpSession::applySettings(rdpSettings* settings) {
// 1. 创建设置 // FreeRDP 3 的键名与 2.x 不同:
rdpSettings* settings = freerdp_settings_new(0); // FreeRDP_ServerHostname / FreeRDP_Username / FreeRDP_DesktopWidth ...
if (!settings) {
running_.store(false);
if (callbacks_.onState) {
callbacks_.onState(false, "无法创建设置");
}
return;
}
// 2. 写入连接参数
// 注意 FreeRDP 3 的键名与 2.x 不同:
// FreeRDP_ServerHostname / FreeRDP_Username / FreeRDP_DesktopWidth ...
freerdp_settings_set_string(settings, FreeRDP_ServerHostname, options_.server.c_str()); freerdp_settings_set_string(settings, FreeRDP_ServerHostname, options_.server.c_str());
if (!options_.username.empty()) { if (!options_.username.empty()) {
freerdp_settings_set_string(settings, FreeRDP_Username, options_.username.c_str()); freerdp_settings_set_string(settings, FreeRDP_Username, options_.username.c_str());
@@ -79,80 +99,99 @@ void RdpSession::run() {
static_cast<UINT32>(options_.width)); static_cast<UINT32>(options_.width));
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight,
static_cast<UINT32>(options_.height)); static_cast<UINT32>(options_.height));
freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, static_cast<UINT32>(options_.bpp));
static_cast<UINT32>(options_.bpp)); freerdp_settings_set_bool(settings, FreeRDP_Fullscreen, options_.fullscreen ? TRUE : FALSE);
freerdp_settings_set_bool(settings, FreeRDP_Fullscreen,
options_.fullscreen ? TRUE : FALSE);
freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate,
options_.ignoreCertificate ? TRUE : FALSE); options_.ignoreCertificate ? TRUE : FALSE);
freerdp_settings_set_bool(settings, FreeRDP_NetworkAutoDetect, TRUE); freerdp_settings_set_bool(settings, FreeRDP_NetworkAutoDetect, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_UseRdpSecurityLayer, TRUE); freerdp_settings_set_bool(settings, FreeRDP_UseRdpSecurityLayer, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_SupportDynamicChannels, TRUE); freerdp_settings_set_bool(settings, FreeRDP_SupportDynamicChannels, TRUE);
// 3. GFX 能力开关 // GFX 能力开关,对应命令行 /gfx 的能力组合语义。
// 对应命令行 /gfx 的能力组合语义: // FreeRDP 3.32.1 单独指定 AVC420 会解析失败,必须成组出现。
// progressive|RFX|AVC420|AVC444
// FreeRDP 3.32.1 单独指定 AVC420 会解析失败,必须成组出现,
// 因此这里按能力逐项开启并保证 progressive 一起生效。
freerdp_settings_set_bool(settings, FreeRDP_GfxProgressive, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxProgressive, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_GfxH264, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxH264, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444v2, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444v2, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_GfxThinClient, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxThinClient, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_GfxSmallCache, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxSmallCache, TRUE);
}
// 4. 创��上下文 void RdpSession::run() {
rdpContext* context = freerdp_context_new(settings); // 1. 创建设置
if (!context) { rdpSettings* settings = freerdp_settings_new(0);
freerdp_settings_free(settings); if (!settings) {
running_.store(false); running_.store(false);
if (callbacks_.onState) { if (callbacks_.onState) {
callbacks_.onState(false, "无法创建 RDP 上下文"); callbacks_.onState(false, "无法创建设置");
} }
return; return;
} }
context_ = context; applySettings(settings);
// 5. 会话槽位(用于 C 回调找回 C++ 对象) // 2. 创建实例
auto* slot = new SessionSlot(); freerdp* instance = freerdp_new();
slot->session = this; if (!instance) {
context->handle = slot; freerdp_settings_free(settings);
running_.store(false);
if (callbacks_.onState) {
callbacks_.onState(false, "无法创建 FreeRDP 实例");
}
return;
}
instance_ = instance;
// 6. 注册事件 // 3. 注册会话对象(按 freerdp* 记录,供静态回调取回)
context->PostConnect = onPostConnect; SessionRegistry::add(instance, this);
context->PostDisconnect = onPostDisconnect;
// 7. 挂载 GFX 渲染管线:把解码后的帧通过回调交给 Qt // 4. 注册事件
rdpGdi* gdi = context->gdi; instance->PostConnect = onPostConnect;
if (gdi && gdi->gfx) { instance->PostDisconnect = onPostDisconnect;
gdi->gfx->UpdateSurfaceArea = onUpdateSurfaceArea; // NLA 认证:把配置中的账号密码交给 FreeRDP
instance->AuthenticateEx = onAuthenticateEx;
// 5. 建立上下文
if (!freerdp_context_new_ex(instance, settings)) {
if (callbacks_.onState) {
callbacks_.onState(false, "无法创建 RDP 上下文");
}
SessionRegistry::remove(instance);
freerdp_free(instance);
freerdp_settings_free(settings);
instance_ = nullptr;
running_.store(false);
return;
}
context_ = instance->context;
// 6. 注册输出回调
// GDI 的初始化(gdi_init)必须放在 PostConnect 里,
// 因为它依赖连接协商后的桌面尺寸与颜色格式。
if (context_->update) {
context_->update->BitmapUpdate = onBitmapUpdate;
context_->update->EndPaint = onEndPaint;
} }
// 8. 连接(阻塞) // 8. 连接(阻塞)
if (!freerdp_connect(context)) { if (!freerdp_connect(instance)) {
if (callbacks_.onState) { if (callbacks_.onState) {
callbacks_.onState(false, "连接失败,请检查地址与网络"); callbacks_.onState(false, "连接失败,请检查地址与网络");
} }
delete slot; SessionRegistry::remove(instance);
context->handle = nullptr; freerdp_context_free(instance);
freerdp_disconnect(context); freerdp_free(instance);
freerdp_context_free(context);
freerdp_settings_free(settings); freerdp_settings_free(settings);
instance_ = nullptr;
context_ = nullptr; context_ = nullptr;
running_.store(false); running_.store(false);
return; return;
} }
// 9. 事件循环:FreeRDP 3 需要手动泵事件 // 9. 事件循环
while (running_.load() && freerdp_get_state(context) != CONNECTION_STATE_FINAL) { while (running_.load() && !freerdp_shall_disconnect_context(context_)) {
if (!freerdp_check_event_handles(context, 100)) { if (!freerdp_check_event_handles(context_)) {
continue; continue;
} }
if (context->rdp->AutoReconnect) {
if (!freerdp_reconnect(context)) {
break;
}
}
} }
connected_.store(false); connected_.store(false);
@@ -160,65 +199,127 @@ void RdpSession::run() {
callbacks_.onState(false, "会话已结束"); callbacks_.onState(false, "会话已结束");
} }
delete slot; SessionRegistry::remove(instance);
context->handle = nullptr; freerdp_context_free(instance);
freerdp_disconnect(context); freerdp_free(instance);
freerdp_context_free(context);
freerdp_settings_free(settings); freerdp_settings_free(settings);
instance_ = nullptr;
context_ = nullptr; context_ = nullptr;
running_.store(false); running_.store(false);
} }
BOOL RdpSession::onPostConnect(rdpContext* context) { BOOL RdpSession::onAuthenticateEx(freerdp* instance, char** username, char** password, char** domain,
auto* slot = slotOf(context); rdp_auth_reason reason) {
if (slot && slot->session) { (void)reason;
slot->session->connected_.store(true); RdpSession* self = sessionOf(instance);
if (slot->session->callbacks_.onState) { if (!self) {
slot->session->callbacks_.onState(true, "已连接"); return FALSE;
}
// 注意:NLA/CredSSP 必须拿到非 nullptr 的凭据指针,
// 空密码也要传空字符串(传 nullptr 会导致 SEC_E_NO_CREDENTIALS)。
// 文档要求终端不长期持有凭据;正式产品改为后端下发一次性票据
// 或由 RDP 网关代理认证。
if (username) {
*username = strdup(self->options_.username.c_str());
}
if (password) {
*password = strdup(self->options_.password.c_str());
}
if (domain) {
*domain = strdup(self->options_.domain.c_str());
}
return TRUE;
}
BOOL RdpSession::onPostConnect(freerdp* instance) {
RdpSession* self = sessionOf(instance);
if (!self) {
return FALSE;
}
// GDI 初始化必须在连接建立后(官方 x11 客户端同样放在 PostConnect)
if (!gdi_init(instance, PIXEL_FORMAT_BGRX32)) {
if (self->callbacks_.onState) {
self->callbacks_.onState(false, "GDI 初始化失败");
}
return FALSE;
}
// 挂载 GFX 渲染管线:把解码后的帧通过回调交给 Qt
if (instance->context && instance->context->gdi && instance->context->gdi->gfx) {
instance->context->gdi->gfx->custom = self;
gdi_graphics_pipeline_init_ex(instance->context->gdi, instance->context->gdi->gfx, nullptr,
nullptr, onUpdateSurfaceArea);
}
self->connected_.store(true);
if (self->callbacks_.onState) {
self->callbacks_.onState(true, "已连接");
}
return TRUE;
}
void RdpSession::onPostDisconnect(freerdp* instance) {
RdpSession* self = sessionOf(instance);
if (self) {
self->connected_.store(false);
}
}
void RdpSession::emitFrame() {
if (!context_ || !context_->gdi) {
return;
}
rdpGdi* gdi = context_->gdi;
if (!gdi->primary_buffer) {
return;
}
frameWidth_ = static_cast<uint32_t>(gdi->width);
frameHeight_ = static_cast<uint32_t>(gdi->height);
frameStride_ = gdi->stride;
frameFormat_ = gdi->dstFormat;
if (callbacks_.onFrame) {
callbacks_.onFrame(gdi->primary_buffer, frameWidth_, frameHeight_, frameStride_,
frameFormat_);
}
}
BOOL RdpSession::onBitmapUpdate(rdpContext* context, const BITMAP_UPDATE* bitmap) {
(void)bitmap;
// GDI 层已经把位图画进 primary buffer,这里只标记有更新
if (context && context->instance) {
RdpSession* self = sessionOf(context->instance);
if (self) {
self->frameDirty_ = true;
} }
} }
return TRUE; return TRUE;
} }
BOOL RdpSession::onPostDisconnect(rdpContext* context) { BOOL RdpSession::onEndPaint(rdpContext* context) {
auto* slot = slotOf(context); // 一帧绘制完成,交给 Qt 刷新
if (slot && slot->session) { if (context && context->instance) {
slot->session->connected_.store(false); RdpSession* self = sessionOf(context->instance);
if (self) {
self->emitFrame();
}
} }
return TRUE; return TRUE;
} }
UINT RdpSession::onUpdateSurfaceArea(RdpgfxClientContext* context, const RECTANGLE_16* rects, UINT RdpSession::onUpdateSurfaceArea(RdpgfxClientContext* gfxContext, UINT16 surfaceId,
UINT16 surfaceId, UINT32 codecId, UINT32 width, UINT32 height, UINT32 nrRects, const RECTANGLE_16* rects) {
const BYTE* data, UINT32 dataLength, UINT64 frameId) {
(void)rects;
(void)surfaceId; (void)surfaceId;
(void)codecId; (void)nrRects;
(void)dataLength; (void)rects;
(void)frameId;
// 通过 gfx 上下文回溯到 rdpContext if (!gfxContext) {
auto* slot = slotOf(context->context);
if (!slot || !slot->session) {
return ERROR_SUCCESS; return ERROR_SUCCESS;
} }
RdpSession* self = slot->session; // custom 字段在建立管线时写入,指向 C++ 会话对象
auto* self = static_cast<RdpSession*>(gfxContext->custom);
// gdi->primary_buffer 保存的是解码后的完整帧 if (!self) {
rdpGdi* gdi = context->context->gdi;
if (!gdi || !gdi->primary_buffer) {
return ERROR_SUCCESS; return ERROR_SUCCESS;
} }
self->emitFrame();
self->frameWidth_ = static_cast<uint32_t>(gdi->width);
self->frameHeight_ = static_cast<uint32_t>(gdi->height);
self->frameStride_ = gdi->stride;
self->frameFormat_ = gdi->dstFormat;
if (self->callbacks_.onFrame) {
self->callbacks_.onFrame(gdi->primary_buffer, self->frameWidth_, self->frameHeight_,
self->frameStride_, self->frameFormat_);
}
return ERROR_SUCCESS; return ERROR_SUCCESS;
} }
+30 -14
View File
@@ -4,10 +4,16 @@
// "以 libfreerdp3 API 内嵌(非拉起外部进程),保证会话内无缝切换与全局热键" // "以 libfreerdp3 API 内嵌(非拉起外部进程),保证会话内无缝切换与全局热键"
// 协议层 FreeRDP 3;图形管线走 GFX;4K 档切 H.265 解码管线 // 协议层 FreeRDP 3;图形管线走 GFX;4K 档切 H.265 解码管线
// //
// 实现路径: // FreeRDP 3 API 结构(与 2.x 差异较大):
// libfreerdp3 核心库提供 freerdp_context_new / gdi_graphics_pipeline_init_ex, // freerdp* instance = freerdp_new(); // 主对象
// 通过 pcRdpgfxUpdateSurfaceArea 回调拿到解码后的帧缓冲,交由 Qt 绘制。 // instance->context = <rdpContext*>; // 内部结构,公开
// 后续 freerdp-rk 分支把这里替换为 GStreamer + mppvideodec 零拷贝管线。 // instance->context->gdi // GDI 渲染状态
// instance->PostConnect / PostDisconnect // 事件回调
// instance->pClientEntryPoints // 客户端入口点
//
// 渲染:gdi_graphics_pipeline_init_ex 的 pcRdpgfxUpdateSurfaceArea 回调
// 拿到解码后的帧缓冲,交由 Qt 绘制。
// 后续 freerdp-rk 分支替换为 GStreamer + mppvideodec 零拷贝管线。
#ifndef KIOSK_SHELL_RDP_SESSION_H #ifndef KIOSK_SHELL_RDP_SESSION_H
#define KIOSK_SHELL_RDP_SESSION_H #define KIOSK_SHELL_RDP_SESSION_H
@@ -16,7 +22,6 @@
#include <functional> #include <functional>
#include <string> #include <string>
#include <thread> #include <thread>
#include <vector>
#include <freerdp/freerdp.h> #include <freerdp/freerdp.h>
#include <freerdp/gdi/gdi.h> #include <freerdp/gdi/gdi.h>
@@ -41,11 +46,11 @@ struct RdpOptions {
}; };
struct RdpCallbacks { struct RdpCallbacks {
// 每帧解码完成,参数:像素数据、宽、高、每行字节数、像素格式 // 每帧解码完成
std::function<void(const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride, std::function<void(const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride,
uint32_t format)> uint32_t format)>
onFrame; onFrame;
// 会话状态:connected 表示当前是否已连上 // 会话状态变化
std::function<void(bool connected, const std::string& message)> onState; std::function<void(bool connected, const std::string& message)> onState;
}; };
@@ -65,24 +70,35 @@ public:
private: private:
void run(); void run();
// FreeRDP 回调(静态,回调内通过 context 取回 this) void applySettings(rdpSettings* settings);
static BOOL onPostConnect(rdpContext* context);
static BOOL onPostDisconnect(rdpContext* context); // FreeRDP 回调(静态,通过 instance->context 扩展结构找回 C++ 对象)
static UINT onUpdateSurfaceArea(RdpgfxClientContext* context, const RDPGFX_RECT16* rect, static BOOL onPostConnect(freerdp* instance);
UINT16 surfaceId, UINT32 codecId, UINT32 width, UINT32 height, static void onPostDisconnect(freerdp* instance);
const BYTE* data, UINT32 dataLength, UINT64 frameId); // NLA 认证回调:把配置中的账号/密码交给 FreeRDP
static BOOL onAuthenticateEx(freerdp* instance, char** username, char** password, char** domain,
rdp_auth_reason reason);
// GDI 输出回调:位图更新与绘制完成
static BOOL onBitmapUpdate(rdpContext* context, const BITMAP_UPDATE* bitmap);
static BOOL onEndPaint(rdpContext* context);
static UINT onUpdateSurfaceArea(RdpgfxClientContext* context, UINT16 surfaceId, UINT32 nrRects,
const RECTANGLE_16* rects);
// 把当前 GDI 帧缓冲交给上层
void emitFrame();
RdpOptions options_; RdpOptions options_;
RdpCallbacks callbacks_; RdpCallbacks callbacks_;
std::atomic<bool> running_{false}; std::atomic<bool> running_{false};
std::atomic<bool> connected_{false}; std::atomic<bool> connected_{false};
freerdp* instance_ = nullptr;
rdpContext* context_ = nullptr; rdpContext* context_ = nullptr;
std::thread worker_; std::thread worker_;
uint8_t* frameBuffer_ = nullptr;
uint32_t frameWidth_ = 0; uint32_t frameWidth_ = 0;
uint32_t frameHeight_ = 0; uint32_t frameHeight_ = 0;
uint32_t frameStride_ = 0; uint32_t frameStride_ = 0;
uint32_t frameFormat_ = 0; uint32_t frameFormat_ = 0;
bool frameDirty_ = false;
}; };
} // namespace vdi } // namespace vdi
+1 -1
View File
@@ -34,7 +34,7 @@ bool StateMachine::isLegalTransition(State from, State to) {
return false; return false;
} }
bool StateMachine::transitionTo(State next, const std::string& detail) { bool StateMachine::transitionTo(State next, const QString& detail) {
if (!isLegalTransition(current_, next)) { if (!isLegalTransition(current_, next)) {
return false; return false;
} }
+4 -2
View File
@@ -15,6 +15,7 @@
#ifndef KIOSK_SHELL_STATE_MACHINE_H #ifndef KIOSK_SHELL_STATE_MACHINE_H
#define KIOSK_SHELL_STATE_MACHINE_H #define KIOSK_SHELL_STATE_MACHINE_H
#include <QString>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -33,7 +34,8 @@ const char* stateToString(State s);
class StateMachine { class StateMachine {
public: public:
using StateListener = std::function<void(State from, State to, const std::string& detail)>; // 状态说明使用 QString,便于直接显示在状态条上
using StateListener = std::function<void(State from, State to, const QString& detail)>;
explicit StateMachine(StateListener listener = nullptr); explicit StateMachine(StateListener listener = nullptr);
@@ -41,7 +43,7 @@ public:
bool isIn(State s) const { return current_ == s; } bool isIn(State s) const { return current_ == s; }
// 尝试切换到目标状态;非法转换会被拒绝并返回 false // 尝试切换到目标状态;非法转换会被拒绝并返回 false
bool transitionTo(State next, const std::string& detail = std::string()); bool transitionTo(State next, const QString& detail = QString());
// 当前状态下允许执行的动作 // 当前状态下允许执行的动作
bool canConnect() const; bool canConnect() const;