功能:接入 FreeRDP 3 内嵌客户端与远程桌面

变更详情: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:
宋一鸣 2026-10-23 18:57:05 +08:00
parent c25bc2908a
commit 24529b80b0
8 changed files with 366 additions and 152 deletions

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 额外源或自研)。

View File

@ -7,6 +7,7 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QProcess>
#include <QPushButton>
#include <QVBoxLayout>
@ -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<int>::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

View File

@ -43,7 +43,6 @@ signals:
private slots:
void onConnectClicked();
void onResolutionChanged(int index);
private:
TerminalConfig cfg_;

View File

@ -14,23 +14,52 @@
#include <QTimer>
#include <QVBoxLayout>
#include <freerdp/codec/color.h>
#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<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;
@ -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<const uchar*>(data), static_cast<int>(width),
static_cast<int>(height), static_cast<int>(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_) {

View File

@ -1,20 +1,50 @@
#include "rdp_session.h"
#include <cstddef>
#include <cstring>
#include <map>
#include <mutex>
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<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) {
if (!context) return nullptr;
auto* slot = static_cast<SessionSlot*>(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<UINT32>(options_.width));
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight,
static_cast<UINT32>(options_.height));
freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth,
static_cast<UINT32>(options_.bpp));
freerdp_settings_set_bool(settings, FreeRDP_Fullscreen,
options_.fullscreen ? TRUE : FALSE);
freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, static_cast<UINT32>(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. 创<><E5889B>上下文
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<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;
}
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<RdpSession*>(gfxContext->custom);
if (!self) {
return ERROR_SUCCESS;
}
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_);
}
self->emitFrame();
return ERROR_SUCCESS;
}

View File

@ -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 = <rdpContext*>; // 内部结构,公开
// 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 <functional>
#include <string>
#include <thread>
#include <vector>
#include <freerdp/freerdp.h>
#include <freerdp/gdi/gdi.h>
@ -41,11 +46,11 @@ struct RdpOptions {
};
struct RdpCallbacks {
// 每帧解码完成,参数:像素数据、宽、高、每行字节数、像素格式
// 每帧解码完成
std::function<void(const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride,
uint32_t format)>
onFrame;
// 会话状态:connected 表示当前是否已连上
// 会话状态变化
std::function<void(bool connected, const std::string& message)> 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<bool> running_{false};
std::atomic<bool> 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

View File

@ -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;
}

View File

@ -15,6 +15,7 @@
#ifndef KIOSK_SHELL_STATE_MACHINE_H
#define KIOSK_SHELL_STATE_MACHINE_H
#include <QString>
#include <functional>
#include <string>
@ -33,7 +34,8 @@ const char* stateToString(State s);
class StateMachine {
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);
@ -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;