功能:接入 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:
parent
c25bc2908a
commit
24529b80b0
67
docs/freerdp3-embedding-notes.md
Normal file
67
docs/freerdp3-embedding-notes.md
Normal 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 额外源或自研)。
|
||||
@ -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
|
||||
|
||||
@ -43,7 +43,6 @@ signals:
|
||||
|
||||
private slots:
|
||||
void onConnectClicked();
|
||||
void onResolutionChanged(int index);
|
||||
|
||||
private:
|
||||
TerminalConfig cfg_;
|
||||
|
||||
@ -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_) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user