Feature: screen preview thumbnail on host double-click

Server sends COMMAND_SCREEN_PREVIEW_REQ when user double-clicks an
active (non-Locked/Inactive) host that advertises CLIENT_CAP_SCREEN_PREVIEW.
Client BitBlts primary screen, encodes to JPEG via GDI+ and replies. The
existing STATIC tooltip is replaced with a self-drawn CPreviewTipWnd
showing the thumbnail above the host info text, with wide-character
rendering so the popup also works on non-Chinese servers.

- Quality tiers reuse QualityProfile pattern: PreviewProfile + 6 levels
  driven by GetTargetQualityLevel (FRP-aware), with 4K/ultrawide auto
  upscale on Ultra/High tiers up to min(screenWidth/4, 1280).
- Client limits to 1 in-flight capture via atomic counter to defend
  against flood/DoS; Send2Server is already mutex-serialized.
- Server validates responses by reqId only (single in-flight tip);
  4s arrival timeout marks "preview unavailable" without blocking the
  text fallback path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuanyuanxiang
2026-05-07 18:17:28 +02:00
parent 70a6b0128e
commit 566f5b8d42
15 changed files with 785 additions and 22 deletions

View File

@@ -125,9 +125,10 @@ inline int isValid_10s()
#define DLL_VERSION __DATE__ // DLL版本
// 客户端能力位
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
// 无此位 = 老客户端,按系统 ANSI默认 CP936解读
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
// 无此位 = 老客户端,按系统 ANSI默认 CP936解读
#define CLIENT_CAP_SCREEN_PREVIEW 0x0004 // 支持屏幕预览(双击在线主机时弹缩略图)
#define TALK_DLG_MAXLEN 1024 // 最大输入字符长度
@@ -334,6 +335,8 @@ enum {
CMD_PEER_TO_PEER = 244, // P2P通信
TOKEN_CLIENTID = 245,
TOKEN_CONN_AUTH = 246, // 子连接身份校验包(客户端首发,服务端回 ConnAuthAck
COMMAND_SCREEN_PREVIEW_REQ = 247, // 屏幕预览请求(服务端→客户端)
TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端)
};
// 子连接校验HMAC 签名 (clientID || timestamp || nonce),服务端通过校验后把 clientID
@@ -374,6 +377,47 @@ struct ConnAuthAck {
static_assert(sizeof(ConnAuthPacket) == 512, "ConnAuthPacket must be exactly 512 bytes");
static_assert(sizeof(ConnAuthAck) == 256, "ConnAuthAck must be exactly 256 bytes");
// 屏幕预览服务端按双击在线主机触发向客户端要一张缩略图JPEG与浮窗一起显示。
// 服务端依 ctx 最近心跳 Ping + RES_RESOLUTION 决定 maxWidth/quality 后下发;客户端
// 主屏抓图 → 等比缩放 → JPEG 编码 → 回响应。format 字段 v1 锁 0=JPEG预留 PNG/WebP。
#pragma pack(push, 1)
struct ScreenPreviewReq {
uint8_t cmd; // = COMMAND_SCREEN_PREVIEW_REQ
uint16_t reqId; // 请求序号,用于丢弃过期响应
uint16_t maxWidth; // 服务端期望的目标宽度(客户端等比缩放,不强制)
uint8_t jpegQuality; // 1..100
uint16_t reserved;
}; // 总 8 字节
enum ScreenPreviewStatus {
SCREEN_PREVIEW_OK = 0,
SCREEN_PREVIEW_CAPTURE_FAILED = 1, // 抓屏失败
SCREEN_PREVIEW_ENCODE_FAILED = 2, // 编码失败
SCREEN_PREVIEW_NOT_SUPPORTED = 3, // 平台不支持
};
enum ScreenPreviewFormat {
SCREEN_PREVIEW_FMT_JPEG = 0,
SCREEN_PREVIEW_FMT_PNG = 1, // 预留
SCREEN_PREVIEW_FMT_WEBP = 2, // 预留
};
struct ScreenPreviewRspHeader {
uint8_t token; // = TOKEN_SCREEN_PREVIEW_RSP
uint16_t reqId; // 回显请求序号
uint8_t status; // ScreenPreviewStatus
uint8_t format; // ScreenPreviewFormatv1 仅 JPEG
uint16_t width; // 实际编码图宽
uint16_t height; // 实际编码图高
uint32_t bytes; // 图像字节数(紧随其后)
uint8_t reserved[3];
// 后接 data[bytes]
}; // 头部 16 字节
#pragma pack(pop)
static_assert(sizeof(ScreenPreviewReq) == 8, "ScreenPreviewReq must be 8 bytes");
static_assert(sizeof(ScreenPreviewRspHeader) == 16, "ScreenPreviewRspHeader must be 16 bytes");
enum ConnAuthStatus {
CONN_AUTH_OK = 0,
CONN_AUTH_BAD_SIZE = 1, // 包长度不对
@@ -970,7 +1014,7 @@ typedef struct LOGIN_INFOR {
{
memset(this, 0, sizeof(LOGIN_INFOR));
bToken = TOKEN_LOGIN;
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8);
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8 | CLIENT_CAP_SCREEN_PREVIEW);
}
LOGIN_INFOR& Speed(unsigned long speed)
{
@@ -1143,6 +1187,29 @@ inline const QualityProfile& GetQualityProfile(int level) {
return g_QualityProfiles[level];
}
// 屏幕预览质量配置(与 QualityLevel 共用 RTT 阈值表,但参数维度不同:缩略图只关心
// 编码尺寸 + JPEG 质量,没有 FPS / 算法等运动视频参数)
struct PreviewProfile {
int maxWidth; // 期望编码宽度(客户端会等比缩放,禁止放大)
int jpegQuality; // JPEG 质量 1..100
};
inline const PreviewProfile& GetScreenPreviewProfile(int level) {
static const PreviewProfile g_PreviewProfiles[QUALITY_COUNT] = {
{ 1024, 85 }, // Ultra: 超清 (LAN/同省4K 源屏可进一步放大到 1280)
{ 800, 80 }, // High: 高清 (跨省直连)
{ 640, 75 }, // Good: 标清 (同国/邻国)
{ 480, 70 }, // Medium: 常规 (大陆间)
{ 384, 60 }, // Low: 低清 (跨洲)
{ 256, 50 }, // Minimal: 最低 (极差网络/卫星链路)
};
if (level < 0 || level >= QUALITY_COUNT) {
static const PreviewProfile fallback = { 480, 70 };
return fallback;
}
return g_PreviewProfiles[level];
}
// 根据RTT获取目标质量等级 (控制端使用)
inline int GetTargetQualityLevel(int rtt, int usingFRP) {
// 根据模式应用不同 RTT阈值 (毫秒)