From 24529b80b017a199b384b8d84912151c4a5783dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E4=B8=80=E9=B8=A3?= <1000000008@qq.com> Date: Fri, 23 Oct 2026 18:57:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=20FreeRDP=203=20=E5=86=85=E5=B5=8C=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E4=B8=8E=E8=BF=9C=E7=A8=8B=E6=A1=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更详情: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 内嵌实现笔记文档 --- docs/freerdp3-embedding-notes.md | 67 +++++ terminal/kiosk-shell/src/login_page.cpp | 31 +-- terminal/kiosk-shell/src/login_page.h | 1 - terminal/kiosk-shell/src/main_window.cpp | 62 +++-- terminal/kiosk-shell/src/rdp_session.cpp | 305 ++++++++++++++------- terminal/kiosk-shell/src/rdp_session.h | 44 ++- terminal/kiosk-shell/src/state_machine.cpp | 2 +- terminal/kiosk-shell/src/state_machine.h | 6 +- 8 files changed, 366 insertions(+), 152 deletions(-) create mode 100644 docs/freerdp3-embedding-notes.md diff --git a/docs/freerdp3-embedding-notes.md b/docs/freerdp3-embedding-notes.md new file mode 100644 index 0000000..0f4de08 --- /dev/null +++ b/docs/freerdp3-embedding-notes.md @@ -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 额外源或自研)。 diff --git a/terminal/kiosk-shell/src/login_page.cpp b/terminal/kiosk-shell/src/login_page.cpp index f6e3f20..59e955f 100644 --- a/terminal/kiosk-shell/src/login_page.cpp +++ b/terminal/kiosk-shell/src/login_page.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -16,22 +17,26 @@ namespace { const char* kResolutionIds[] = {"1920x1440", "2560x1440", "1920x1080", "1280x720"}; QString detectNetworkSummary() { - // 简单读取默认路由与第一块非回环网卡地址 + // 取默认路由的出接口与该接口上的 IPv4 地址 QProcess process; process.start("sh", QStringList() << "-c" << - "ip -4 route show default | awk '{print $NF}'; " - "ip -4 -o addr show scope global | awk '{print $4}' | head -n 1"); + "dev=$(ip -4 route show default | awk '{print $NF}' | head -n 1); " + "addr=$(ip -4 -o addr show dev \"$dev\" | awk '{print $4}' | head -n 1); " + "echo \"$dev $addr\""); process.waitForFinished(2000); const QString out = QString::fromUtf8(process.readAllStandardOutput()).trimmed(); if (out.isEmpty()) { return QStringLiteral("网络:未连接"); } - QStringList lines = out.split('\n', Qt::SkipEmptyParts); - QString iface = lines.value(0); - QString addr = lines.value(1); + const QStringList parts = out.split(' ', Qt::SkipEmptyParts); + QString iface = parts.value(0); + QString addr = parts.value(1); if (addr.contains('/')) { addr = addr.split('/').value(0); } + if (iface.isEmpty()) { + return QStringLiteral("网络:未连接"); + } return QStringLiteral("网络:%1 (%2)").arg(iface, addr); } @@ -62,7 +67,7 @@ LoginPage::LoginPage(const TerminalConfig& initial, QWidget* parent) // ---- 设备信息条 ---- 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); labelSn_->setStyleSheet(QStringLiteral("color: #8a94a6;")); labelNetwork_->setStyleSheet(QStringLiteral("color: #8a94a6;")); @@ -77,11 +82,11 @@ LoginPage::LoginPage(const TerminalConfig& initial, QWidget* parent) grid->setVerticalSpacing(12); 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")); - editUser_ = new QLineEdit(cfg_.username, this); + editUser_ = new QLineEdit(QString::fromStdString(cfg_.username), this); editUser_->setPlaceholderText(QStringLiteral("登录账号")); - editPassword_ = new QLineEdit(cfg_.password, this); + editPassword_ = new QLineEdit(QString::fromStdString(cfg_.password), this); editPassword_->setEchoMode(QLineEdit::Password); editPassword_->setPlaceholderText(QStringLiteral("登录密码(可留空)")); @@ -143,8 +148,6 @@ LoginPage::LoginPage(const TerminalConfig& initial, QWidget* parent) connect(buttonConnect_, &QPushButton::clicked, this, &LoginPage::onConnectClicked); connect(buttonQuit_, &QPushButton::clicked, this, [this]() { emit quitRequested(); }); - connect(comboResolution_, QOverload::of(&LoginPage::onResolutionChanged), this, - &LoginPage::onResolutionChanged); } void LoginPage::setConnectHandler(ConnectHandler handler) { @@ -195,8 +198,4 @@ void LoginPage::onConnectClicked() { emit connectRequested(cfg); } -void LoginPage::onResolutionChanged(int) { - // 分辨率变更即时生效,实际尺寸在建立连接时读取 -} - } // namespace vdi diff --git a/terminal/kiosk-shell/src/login_page.h b/terminal/kiosk-shell/src/login_page.h index 4d9fb37..482e62e 100644 --- a/terminal/kiosk-shell/src/login_page.h +++ b/terminal/kiosk-shell/src/login_page.h @@ -43,7 +43,6 @@ signals: private slots: void onConnectClicked(); - void onResolutionChanged(int index); private: TerminalConfig cfg_; diff --git a/terminal/kiosk-shell/src/main_window.cpp b/terminal/kiosk-shell/src/main_window.cpp index 85f4b04..6669898 100644 --- a/terminal/kiosk-shell/src/main_window.cpp +++ b/terminal/kiosk-shell/src/main_window.cpp @@ -14,23 +14,52 @@ #include #include +#include + #include "login_page.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) { - switch (freerdpFormat) { - case 0x00000020: // PIXEL_FORMAT_BGRX8888 - return QImage::Format_RGB888; - case 0x00000030: // PIXEL_FORMAT_BGRA8888 - return QImage::Format_RGBA8888; - case 0x00000000: // PIXEL_FORMAT_RGBX8888 - return QImage::Format_RGBX8888; - default: - return QImage::Format_RGBA8888; + (void)freerdpFormat; + return QImage::Format_RGB888; +} + +// 把 FreeRDP 的 4 字节/像素缓冲转成 QImage 可用的 RGB888 +// bytesPerLine 是 FreeRDP 的每行字节数(width*4) +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); + + QImage out(static_cast(width), static_cast(height), QImage::Format_RGB888); + for (uint32_t y = 0; y < height; ++y) { + const uint8_t* src = data + static_cast(y) * bytesPerLine; + uint8_t* dst = out.scanLine(static_cast(y)); + for (uint32_t x = 0; x < width; ++x) { + const uint8_t* p = src + static_cast(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; @@ -39,8 +68,8 @@ constexpr int kMaxReconnectAttempts = 5; } // namespace MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { - fsm_ = StateMachine([this](State from, State to, const std::string& detail) { - onStateChanged(from, to, QString::fromStdString(detail)); + fsm_ = StateMachine([this](State from, State to, const QString& detail) { + onStateChanged(from, to, detail); }); setupUi(); 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) { return; } - const QImage::Format fmt = toQImageFormat(format); - // FreeRDP 的 primary_buffer 是 BGRA/BGRX 排列,QImage 需要拷贝 - QImage image(reinterpret_cast(data), static_cast(width), - static_cast(height), static_cast(stride), fmt); - lastFrame_ = image.copy(); // 拷贝,脱离 FreeRDP 缓冲生命周期 + // FreeRDP 主缓冲是 4 字节/像素,需转换后交给 Qt + QImage image = convertFrame(data, width, height, stride, format); + if (image.isNull()) { + return; + } + lastFrame_ = image; frameDirty_ = true; if (sessionView_) { diff --git a/terminal/kiosk-shell/src/rdp_session.cpp b/terminal/kiosk-shell/src/rdp_session.cpp index 918d0c3..7f8723d 100644 --- a/terminal/kiosk-shell/src/rdp_session.cpp +++ b/terminal/kiosk-shell/src/rdp_session.cpp @@ -1,20 +1,50 @@ #include "rdp_session.h" +#include #include +#include #include namespace vdi { namespace { -// 会话实例指针表:通过 rdpContext->handle 取回 C++ 对象 -struct SessionSlot { - RdpSession* session = nullptr; +// 从 freerdp 实例取回 C++ 会话对象。 +// 注意:不能用 instance->ContextSize 扩展 rdpContext —— +// 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 lock(mutex()); + table()[instance] = session; + } + static void remove(freerdp* instance) { + std::lock_guard lock(mutex()); + table().erase(instance); + } + static RdpSession* get(freerdp* instance) { + if (!instance) return nullptr; + std::lock_guard 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& table() { + static std::map t; + return t; + } }; -SessionSlot* slotOf(rdpContext* context) { - if (!context) return nullptr; - auto* slot = static_cast(context->handle); - return slot; +RdpSession* sessionOf(freerdp* instance) { + return SessionRegistry::get(instance); } } // namespace @@ -41,30 +71,20 @@ void RdpSession::stop() { return; } running_.store(false); - if (context_) { - // 请求断开;连接线程会在 freerdp_disconnect 后返回 - freerdp_disconnect(context_); + if (instance_) { + // 请求断开;连接线程会从事件循环返回 + freerdp_disconnect(instance_); } if (worker_.joinable()) { worker_.join(); } + instance_ = nullptr; context_ = nullptr; } -void RdpSession::run() { - // 1. 创建设置 - rdpSettings* settings = freerdp_settings_new(0); - if (!settings) { - running_.store(false); - if (callbacks_.onState) { - callbacks_.onState(false, "无法创建设置"); - } - return; - } - - // 2. 写入连接参数 - // 注意 FreeRDP 3 的键名与 2.x 不同: - // FreeRDP_ServerHostname / FreeRDP_Username / FreeRDP_DesktopWidth ... +void RdpSession::applySettings(rdpSettings* settings) { + // FreeRDP 3 的键名与 2.x 不同: + // FreeRDP_ServerHostname / FreeRDP_Username / FreeRDP_DesktopWidth ... freerdp_settings_set_string(settings, FreeRDP_ServerHostname, options_.server.c_str()); if (!options_.username.empty()) { freerdp_settings_set_string(settings, FreeRDP_Username, options_.username.c_str()); @@ -79,80 +99,99 @@ void RdpSession::run() { static_cast(options_.width)); freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, static_cast(options_.height)); - freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, - static_cast(options_.bpp)); - freerdp_settings_set_bool(settings, FreeRDP_Fullscreen, - options_.fullscreen ? TRUE : FALSE); + freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, static_cast(options_.bpp)); + freerdp_settings_set_bool(settings, FreeRDP_Fullscreen, options_.fullscreen ? TRUE : FALSE); freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, options_.ignoreCertificate ? TRUE : FALSE); freerdp_settings_set_bool(settings, FreeRDP_NetworkAutoDetect, TRUE); freerdp_settings_set_bool(settings, FreeRDP_UseRdpSecurityLayer, TRUE); freerdp_settings_set_bool(settings, FreeRDP_SupportDynamicChannels, TRUE); - // 3. GFX 能力开关 - // 对应命令行 /gfx 的能力组合语义: - // progressive|RFX|AVC420|AVC444 - // FreeRDP 3.32.1 单独指定 AVC420 会解析失败,必须成组出现, - // 因此这里按能力逐项开启并保证 progressive 一起生效。 + // GFX 能力开关,对应命令行 /gfx 的能力组合语义。 + // FreeRDP 3.32.1 单独指定 AVC420 会解析失败,必须成组出现。 freerdp_settings_set_bool(settings, FreeRDP_GfxProgressive, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxH264, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444v2, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxThinClient, TRUE); freerdp_settings_set_bool(settings, FreeRDP_GfxSmallCache, TRUE); +} - // 4. 创��上下文 - rdpContext* context = freerdp_context_new(settings); - if (!context) { - freerdp_settings_free(settings); +void RdpSession::run() { + // 1. 创建设置 + rdpSettings* settings = freerdp_settings_new(0); + if (!settings) { running_.store(false); if (callbacks_.onState) { - callbacks_.onState(false, "无法创建 RDP 上下文"); + callbacks_.onState(false, "无法创建设置"); } return; } - context_ = context; + applySettings(settings); - // 5. 会话槽位(用于 C 回调找回 C++ 对象) - auto* slot = new SessionSlot(); - slot->session = this; - context->handle = slot; + // 2. 创建实例 + freerdp* instance = freerdp_new(); + if (!instance) { + freerdp_settings_free(settings); + running_.store(false); + if (callbacks_.onState) { + callbacks_.onState(false, "无法创建 FreeRDP 实例"); + } + return; + } + instance_ = instance; - // 6. 注册事件 - context->PostConnect = onPostConnect; - context->PostDisconnect = onPostDisconnect; + // 3. 注册会话对象(按 freerdp* 记录,供静态回调取回) + SessionRegistry::add(instance, this); - // 7. 挂载 GFX 渲染管线:把解码后的帧通过回调交给 Qt - rdpGdi* gdi = context->gdi; - if (gdi && gdi->gfx) { - gdi->gfx->UpdateSurfaceArea = onUpdateSurfaceArea; + // 4. 注册事件 + instance->PostConnect = onPostConnect; + instance->PostDisconnect = onPostDisconnect; + // 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. 连接(阻塞) - if (!freerdp_connect(context)) { + if (!freerdp_connect(instance)) { if (callbacks_.onState) { callbacks_.onState(false, "连接失败,请检查地址与网络"); } - delete slot; - context->handle = nullptr; - freerdp_disconnect(context); - freerdp_context_free(context); + SessionRegistry::remove(instance); + freerdp_context_free(instance); + freerdp_free(instance); freerdp_settings_free(settings); + instance_ = nullptr; context_ = nullptr; running_.store(false); return; } - // 9. 事件循环:FreeRDP 3 需要手动泵事件 - while (running_.load() && freerdp_get_state(context) != CONNECTION_STATE_FINAL) { - if (!freerdp_check_event_handles(context, 100)) { + // 9. 事件循环 + while (running_.load() && !freerdp_shall_disconnect_context(context_)) { + if (!freerdp_check_event_handles(context_)) { continue; } - if (context->rdp->AutoReconnect) { - if (!freerdp_reconnect(context)) { - break; - } - } } connected_.store(false); @@ -160,65 +199,127 @@ void RdpSession::run() { callbacks_.onState(false, "会话已结束"); } - delete slot; - context->handle = nullptr; - freerdp_disconnect(context); - freerdp_context_free(context); + SessionRegistry::remove(instance); + freerdp_context_free(instance); + freerdp_free(instance); freerdp_settings_free(settings); + instance_ = nullptr; context_ = nullptr; running_.store(false); } -BOOL RdpSession::onPostConnect(rdpContext* context) { - auto* slot = slotOf(context); - if (slot && slot->session) { - slot->session->connected_.store(true); - if (slot->session->callbacks_.onState) { - slot->session->callbacks_.onState(true, "已连接"); +BOOL RdpSession::onAuthenticateEx(freerdp* instance, char** username, char** password, char** domain, + rdp_auth_reason reason) { + (void)reason; + RdpSession* self = sessionOf(instance); + if (!self) { + 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(gdi->width); + frameHeight_ = static_cast(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; } -BOOL RdpSession::onPostDisconnect(rdpContext* context) { - auto* slot = slotOf(context); - if (slot && slot->session) { - slot->session->connected_.store(false); +BOOL RdpSession::onEndPaint(rdpContext* context) { + // 一帧绘制完成,交给 Qt 刷新 + if (context && context->instance) { + RdpSession* self = sessionOf(context->instance); + if (self) { + self->emitFrame(); + } } return TRUE; } -UINT RdpSession::onUpdateSurfaceArea(RdpgfxClientContext* context, const RECTANGLE_16* rects, - UINT16 surfaceId, UINT32 codecId, UINT32 width, UINT32 height, - const BYTE* data, UINT32 dataLength, UINT64 frameId) { - (void)rects; +UINT RdpSession::onUpdateSurfaceArea(RdpgfxClientContext* gfxContext, UINT16 surfaceId, + UINT32 nrRects, const RECTANGLE_16* rects) { (void)surfaceId; - (void)codecId; - (void)dataLength; - (void)frameId; + (void)nrRects; + (void)rects; - // 通过 gfx 上下文回溯到 rdpContext - auto* slot = slotOf(context->context); - if (!slot || !slot->session) { + if (!gfxContext) { return ERROR_SUCCESS; } - RdpSession* self = slot->session; - - // gdi->primary_buffer 保存的是解码后的完整帧 - rdpGdi* gdi = context->context->gdi; - if (!gdi || !gdi->primary_buffer) { + // custom 字段在建立管线时写入,指向 C++ 会话对象 + auto* self = static_cast(gfxContext->custom); + if (!self) { return ERROR_SUCCESS; } - - self->frameWidth_ = static_cast(gdi->width); - self->frameHeight_ = static_cast(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_); - } + self->emitFrame(); return ERROR_SUCCESS; } diff --git a/terminal/kiosk-shell/src/rdp_session.h b/terminal/kiosk-shell/src/rdp_session.h index 1890361..ad61627 100644 --- a/terminal/kiosk-shell/src/rdp_session.h +++ b/terminal/kiosk-shell/src/rdp_session.h @@ -4,10 +4,16 @@ // "以 libfreerdp3 API 内嵌(非拉起外部进程),保证会话内无缝切换与全局热键" // 协议层 FreeRDP 3;图形管线走 GFX;4K 档切 H.265 解码管线 // -// 实现路径: -// libfreerdp3 核心库提供 freerdp_context_new / gdi_graphics_pipeline_init_ex, -// 通过 pcRdpgfxUpdateSurfaceArea 回调拿到解码后的帧缓冲,交由 Qt 绘制。 -// 后续 freerdp-rk 分支把这里替换为 GStreamer + mppvideodec 零拷贝管线。 +// FreeRDP 3 API 结构(与 2.x 差异较大): +// freerdp* instance = freerdp_new(); // 主对象 +// instance->context = ; // 内部结构,公开 +// 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 #define KIOSK_SHELL_RDP_SESSION_H @@ -16,7 +22,6 @@ #include #include #include -#include #include #include @@ -41,11 +46,11 @@ struct RdpOptions { }; struct RdpCallbacks { - // 每帧解码完成,参数:像素数据、宽、高、每行字节数、像素格式 + // 每帧解码完成 std::function onFrame; - // 会话状态:connected 表示当前是否已连上 + // 会话状态变化 std::function onState; }; @@ -65,24 +70,35 @@ public: private: void run(); - // FreeRDP 回调(静态,回调内通过 context 取回 this) - static BOOL onPostConnect(rdpContext* context); - static BOOL onPostDisconnect(rdpContext* context); - static UINT onUpdateSurfaceArea(RdpgfxClientContext* context, const RDPGFX_RECT16* rect, - UINT16 surfaceId, UINT32 codecId, UINT32 width, UINT32 height, - const BYTE* data, UINT32 dataLength, UINT64 frameId); + void applySettings(rdpSettings* settings); + + // FreeRDP 回调(静态,通过 instance->context 扩展结构找回 C++ 对象) + static BOOL onPostConnect(freerdp* instance); + static void onPostDisconnect(freerdp* instance); + // 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_; RdpCallbacks callbacks_; std::atomic running_{false}; std::atomic connected_{false}; + freerdp* instance_ = nullptr; rdpContext* context_ = nullptr; std::thread worker_; - uint8_t* frameBuffer_ = nullptr; uint32_t frameWidth_ = 0; uint32_t frameHeight_ = 0; uint32_t frameStride_ = 0; uint32_t frameFormat_ = 0; + bool frameDirty_ = false; }; } // namespace vdi diff --git a/terminal/kiosk-shell/src/state_machine.cpp b/terminal/kiosk-shell/src/state_machine.cpp index bab70d5..df9bd2b 100644 --- a/terminal/kiosk-shell/src/state_machine.cpp +++ b/terminal/kiosk-shell/src/state_machine.cpp @@ -34,7 +34,7 @@ bool StateMachine::isLegalTransition(State from, State to) { return false; } -bool StateMachine::transitionTo(State next, const std::string& detail) { +bool StateMachine::transitionTo(State next, const QString& detail) { if (!isLegalTransition(current_, next)) { return false; } diff --git a/terminal/kiosk-shell/src/state_machine.h b/terminal/kiosk-shell/src/state_machine.h index c54e700..21f6193 100644 --- a/terminal/kiosk-shell/src/state_machine.h +++ b/terminal/kiosk-shell/src/state_machine.h @@ -15,6 +15,7 @@ #ifndef KIOSK_SHELL_STATE_MACHINE_H #define KIOSK_SHELL_STATE_MACHINE_H +#include #include #include @@ -33,7 +34,8 @@ const char* stateToString(State s); class StateMachine { public: - using StateListener = std::function; + // 状态说明使用 QString,便于直接显示在状态条上 + using StateListener = std::function; explicit StateMachine(StateListener listener = nullptr); @@ -41,7 +43,7 @@ public: bool isIn(State s) const { return current_ == s; } // 尝试切换到目标状态;非法转换会被拒绝并返回 false - bool transitionTo(State next, const std::string& detail = std::string()); + bool transitionTo(State next, const QString& detail = QString()); // 当前状态下允许执行的动作 bool canConnect() const;