From 63ef75b7ce61d48127734a0af7907541ade2eff5 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sun, 14 Jun 2026 00:24:46 +0200 Subject: [PATCH] Feat: ROI screen capture with remote control support via COMMAND_SCREEN_ROI Co-Authored-By: Claude Sonnet 4.6 --- client/ScreenCapture.h | 54 +++++++++++++++++++++++++++++-------- client/ScreenCapturerDXGI.h | 35 ++++++++++++++++-------- client/ScreenManager.cpp | 21 +++++++++++---- client/ScreenManager.h | 4 ++- client/ScreenSpy.cpp | 15 +++++++++-- client/ScreenSpy.h | 3 ++- common/commands.h | 1 + 7 files changed, 102 insertions(+), 31 deletions(-) diff --git a/client/ScreenCapture.h b/client/ScreenCapture.h index 969d27a..dfac8a3 100644 --- a/client/ScreenCapture.h +++ b/client/ScreenCapture.h @@ -168,6 +168,11 @@ public: int m_nInstructionSet = 0; int m_nBitRate = 0; // H264 编码码率 (kbps), 0=自动 + // 感兴趣区域 (ROI) + RECT m_ROI = {0,0,0,0}; + int m_nScaleSendWidth = 0; + int m_nScaleSendHeight = 0; + // 自定义光标相关 DWORD m_dwLastCursorHash = 0; // 上次发送的光标哈希 DWORD m_dwLastCursorSendTime = 0; // 上次发送光标的时间 @@ -192,7 +197,8 @@ protected: int m_nVScreenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN); public: - ScreenCapture(int n = 32, BYTE algo = ALGORITHM_DIFF, BOOL all = FALSE, int level = LEVEL_H264_SOFT) : + ScreenCapture(int n = 32, BYTE algo = ALGORITHM_DIFF, BOOL all = FALSE, int level = LEVEL_H264_SOFT, + RECT rc = {0}, BOOL switchScreen = TRUE) : m_ThreadPool(nullptr), m_FirstBuffer(nullptr), m_RectBuffer(nullptr), m_BitmapInfor_Full(nullptr), m_bAlgorithm(algo), m_SendQuality(100), m_ulFullWidth(0), m_ulFullHeight(0), m_bZoomed(false), m_wZoom(1), m_hZoom(1), @@ -202,6 +208,7 @@ public: m_bLastFrameWasScroll(false), m_nScrollDetectInterval(1), m_EncodeLevel(level) { SetAlgorithm(algo); + m_ROI = rc; m_BitmapInfor_Send = nullptr; m_BmpZoomBuffer = nullptr; m_BmpZoomFirst = nullptr; @@ -212,7 +219,7 @@ public: m_nScreenCount = monitors.size(); m_bEnableMultiScreen = all; if (all && !monitors.empty()) { - int idx = index++ % (monitors.size()+1); + int idx = (switchScreen ? index++ : index) % (monitors.size()+1); if (idx == 0) { m_iScreenX = GetSystemMetrics(SM_XVIRTUALSCREEN); m_iScreenY = GetSystemMetrics(SM_YVIRTUALSCREEN); @@ -899,7 +906,8 @@ public: bool shouldDetectScroll = !keyFrame && algo != ALGORITHM_H264 && m_bEnableScrollDetect && m_bServerSupportsScroll && m_pScrollDetector && !m_bLastFrameWasScroll && m_nScrollDetectInterval > 0 && - (m_FrameID % m_nScrollDetectInterval == 0); + (m_FrameID % m_nScrollDetectInterval == 0) && + !m_nScaleSendWidth; if (shouldDetectScroll) { int scrollAmount = m_pScrollDetector->DetectVerticalScroll(GetFirstBuffer(), nextData); @@ -1045,9 +1053,14 @@ public: // 鼠标位置转换:将服务端坐标(基于发送分辨率)转换为客户端坐标(原始分辨率) virtual void PointConversion(POINT& pt) const { + // 0. ROI 偏移:ROI 坐标系 → scale 坐标系 + if (m_nScaleSendWidth) { + pt.x += m_ROI.left; + pt.y += m_ROI.top; + } // 1. 处理图像缩小传输的坐标缩放(maxWidth 限制) - int sendWidth = m_BitmapInfor_Send->bmiHeader.biWidth; - int sendHeight = m_BitmapInfor_Send->bmiHeader.biHeight; + int sendWidth = m_nScaleSendWidth ? m_nScaleSendWidth : (int)m_BitmapInfor_Send->bmiHeader.biWidth; + int sendHeight = m_nScaleSendHeight ? m_nScaleSendHeight : (int)m_BitmapInfor_Send->bmiHeader.biHeight; if (sendWidth != m_ulFullWidth || sendHeight != m_ulFullHeight) { pt.x = (LONG)(pt.x * (double)m_ulFullWidth / sendWidth + 0.5); pt.y = (LONG)(pt.y * (double)m_ulFullHeight / sendHeight + 0.5); @@ -1074,12 +1087,17 @@ public: pt.y = (LONG)(pt.y / m_hZoom); } // 1'. full → send 缩放(位图下采样传输时) - int sendWidth = m_BitmapInfor_Send->bmiHeader.biWidth; - int sendHeight = m_BitmapInfor_Send->bmiHeader.biHeight; + int sendWidth = m_nScaleSendWidth ? m_nScaleSendWidth : (int)m_BitmapInfor_Send->bmiHeader.biWidth; + int sendHeight = m_nScaleSendHeight ? m_nScaleSendHeight : (int)m_BitmapInfor_Send->bmiHeader.biHeight; if (sendWidth != (int)m_ulFullWidth || sendHeight != (int)m_ulFullHeight) { pt.x = (LONG)((double)pt.x * sendWidth / m_ulFullWidth + 0.5); pt.y = (LONG)((double)pt.y * sendHeight / m_ulFullHeight + 0.5); } + // 0'. scale 坐标系 → ROI 坐标系 + if (m_nScaleSendWidth) { + pt.x -= m_ROI.left; + pt.y -= m_ROI.top; + } } // 获取位图结构信息 @@ -1164,11 +1182,25 @@ public: // 纯虚接口 // 获取下一帧屏幕 virtual LPBYTE ScanNextScreen() = 0; + // ROI crop:从 src(scale 后缓冲)裁剪到 target;src==target 时 in-place 安全 + LPBYTE applyROICrop(LPBYTE target, LPBYTE src, int scaledW, int scaledH) + { + int rw = m_BitmapInfor_Send->bmiHeader.biWidth, rh = m_BitmapInfor_Send->bmiHeader.biHeight; + for (int row = 0; row < rh; row++) + memmove(target + row * rw * 4, src + ((scaledH - m_ROI.bottom + row) * scaledW + m_ROI.left) * 4, rw * 4); + return target; + } + virtual LPBYTE scaleBitmap(LPBYTE target, LPBYTE bitmap) { - if (m_ulFullWidth == m_BitmapInfor_Send->bmiHeader.biWidth && m_ulFullHeight == m_BitmapInfor_Send->bmiHeader.biHeight) - return bitmap; - return ScaleBitmap(target, (uint8_t*)bitmap, m_ulFullWidth, m_ulFullHeight, m_BitmapInfor_Send->bmiHeader.biWidth, - m_BitmapInfor_Send->bmiHeader.biHeight, m_nInstructionSet); + int scaledW = m_nScaleSendWidth ? m_nScaleSendWidth : (int)m_BitmapInfor_Send->bmiHeader.biWidth; + int scaledH = m_nScaleSendHeight ? m_nScaleSendHeight : (int)m_BitmapInfor_Send->bmiHeader.biHeight; + LPBYTE src = bitmap; + if ((ULONG)scaledW != m_ulFullWidth || (ULONG)scaledH != m_ulFullHeight) + src = ScaleBitmap(target, (uint8_t*)bitmap, m_ulFullWidth, m_ulFullHeight, scaledW, scaledH, m_nInstructionSet); + if (m_nScaleSendWidth) { + src = applyROICrop(target, src, scaledW, scaledH); + } + return src; } }; diff --git a/client/ScreenCapturerDXGI.h b/client/ScreenCapturerDXGI.h index 6ee4d1f..aa49a1a 100644 --- a/client/ScreenCapturerDXGI.h +++ b/client/ScreenCapturerDXGI.h @@ -25,11 +25,12 @@ private: BYTE* m_NextBuffer = nullptr; public: - ScreenCapturerDXGI(BYTE algo, int gop = DEFAULT_GOP, BOOL all = FALSE, int level = LEVEL_H264_SOFT) - : ScreenCapture(32, algo, all, level) + ScreenCapturerDXGI(BYTE algo, int gop = DEFAULT_GOP, BOOL all = FALSE, int level = LEVEL_H264_SOFT, + RECT rc = {0}, BOOL switchScreen = TRUE) + : ScreenCapture(32, algo, all, level, rc, switchScreen) { m_GOP = gop; - InitDXGI(all); + InitDXGI(all, switchScreen); Mprintf("Capture screen with DXGI: GOP= %d\n", m_GOP); } @@ -47,7 +48,7 @@ public: return TRUE; } - void InitDXGI(BOOL all) + void InitDXGI(BOOL all, BOOL switchScreen) { m_iScreenX = 0; m_iScreenY = 0; @@ -72,7 +73,7 @@ public: // 4. 获取 DXGI 输出(屏幕) static UINT screen = 0; - HRESULT r = dxgiAdapter->EnumOutputs(screen++, &dxgiOutput); + HRESULT r = dxgiAdapter->EnumOutputs(switchScreen ? screen++ : screen, &dxgiOutput); if (r == DXGI_ERROR_NOT_FOUND && all) { screen = 0; idx ++; @@ -148,6 +149,15 @@ public: m_BmpZoomBuffer = new BYTE[m_BitmapInfor_Send->bmiHeader.biSizeImage * 2 + 12]; m_BmpZoomFirst = nullptr; + // ROI + int w = m_ROI.right - m_ROI.left, h = m_ROI.bottom - m_ROI.top; + if (w > 0 && h > 0) { + m_nScaleSendWidth = m_BitmapInfor_Send->bmiHeader.biWidth; + m_nScaleSendHeight = m_BitmapInfor_Send->bmiHeader.biHeight; + m_BitmapInfor_Send->bmiHeader.biWidth = w; + m_BitmapInfor_Send->bmiHeader.biHeight = h; + m_BitmapInfor_Send->bmiHeader.biSizeImage = w * h * 4; + } break; } while (true); @@ -173,12 +183,15 @@ public: virtual LPBYTE scaleBitmap(LPBYTE target, LPBYTE bitmap) override { - if (m_ulFullWidth == m_BitmapInfor_Send->bmiHeader.biWidth && m_ulFullHeight == m_BitmapInfor_Send->bmiHeader.biHeight) { - memcpy(target, bitmap, m_BitmapInfor_Send->bmiHeader.biSizeImage); - return bitmap; - } - return ScaleBitmap(target, (uint8_t*)bitmap, m_ulFullWidth, m_ulFullHeight, m_BitmapInfor_Send->bmiHeader.biWidth, - m_BitmapInfor_Send->bmiHeader.biHeight, m_nInstructionSet); + int scaledW = m_nScaleSendWidth ? m_nScaleSendWidth : (int)m_BitmapInfor_Send->bmiHeader.biWidth; + int scaledH = m_nScaleSendHeight ? m_nScaleSendHeight : (int)m_BitmapInfor_Send->bmiHeader.biHeight; + if ((ULONG)scaledW == m_ulFullWidth && (ULONG)scaledH == m_ulFullHeight) + memcpy(target, bitmap, scaledW * scaledH * 4); + else + ScaleBitmap(target, (uint8_t*)bitmap, m_ulFullWidth, m_ulFullHeight, scaledW, scaledH, m_nInstructionSet); + if (m_nScaleSendWidth) + applyROICrop(target, target, scaledW, scaledH); + return target; } LPBYTE GetFirstScreenData(ULONG* ulFirstScreenLength) override diff --git a/client/ScreenManager.cpp b/client/ScreenManager.cpp index e81250f..97f502e 100644 --- a/client/ScreenManager.cpp +++ b/client/ScreenManager.cpp @@ -173,10 +173,10 @@ bool CScreenManager::SwitchScreen() if (m_ScreenSpyObject == NULL || m_ScreenSpyObject->GetScreenCount() <= 1 || !m_ScreenSpyObject->IsMultiScreenEnabled()) return false; - return RestartScreen(); + return RestartScreen(TRUE); } -bool CScreenManager::RestartScreen() +bool CScreenManager::RestartScreen(BOOL switchScreen) { if (m_ScreenSpyObject == NULL || m_bIsWorking == FALSE) return false; @@ -194,6 +194,7 @@ bool CScreenManager::RestartScreen() // 3. 重新启动工作线程(InitScreenSpy 会创建新对象) m_bIsWorking = TRUE; m_SendFirst = FALSE; + m_SwitchScreen = switchScreen; m_hWorkThread = __CreateThread(NULL, 0, WorkThreadProc, this, 0, NULL); return true; } @@ -462,6 +463,8 @@ void CScreenManager::InitScreenSpy() BYTE algo = ALGORITHM_DIFF; BYTE* user = (BYTE*)m_ptrUser; BOOL all = FALSE; + RECT rect = m_ROI; + BOOL switchScreen = m_SwitchScreen; if (!(user == NULL || ((int)user) == 1)) { UserParam* param = (UserParam*)user; if (param) { @@ -522,18 +525,18 @@ void CScreenManager::InitScreenSpy() SAFE_DELETE(m_ScreenSpyObject); if ((USING_DXGI == DXGI && IsWindows8orHigher())) { m_isGDI = FALSE; - auto s = new ScreenCapturerDXGI(algo, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel); + auto s = new ScreenCapturerDXGI(algo, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel, rect, switchScreen); if (s->IsInitSucceed()) { m_ScreenSpyObject = s; } else { SAFE_DELETE(s); m_isGDI = TRUE; - m_ScreenSpyObject = new CScreenSpy(32, algo, FALSE, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel); + m_ScreenSpyObject = new CScreenSpy(32, algo, FALSE, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel, rect, switchScreen); Mprintf("CScreenManager: DXGI SPY init failed!!! Using GDI instead.\n"); } } else { m_isGDI = TRUE; - m_ScreenSpyObject = new CScreenSpy(32, algo, DXGI == USING_VIRTUAL, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel); + m_ScreenSpyObject = new CScreenSpy(32, algo, DXGI == USING_VIRTUAL, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel, rect, switchScreen); } } @@ -820,6 +823,14 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength) m_ClientObject->StopRunning(); break; } + case COMMAND_SCREEN_ROI:{ + if (ulLength > sizeof(RECT)) { + memcpy(&m_ROI, szBuffer + 1, sizeof(RECT)); + Mprintf("[CScreenManager] Set ROI: (%d, %d), (%d, %d)\n", m_ROI.left, m_ROI.top, m_ROI.right, m_ROI.bottom); + RestartScreen(); + } + break; + } case COMMAND_ENCODE_LEVEL: { int encodeLevel = szBuffer[1]; iniFile cfg(CLIENT_PATH); diff --git a/client/ScreenManager.h b/client/ScreenManager.h index 3af91f8..d646cfd 100644 --- a/client/ScreenManager.h +++ b/client/ScreenManager.h @@ -63,6 +63,8 @@ public: std::string m_DesktopID; BOOL m_bIsWorking; BOOL m_bIsBlockInput; + RECT m_ROI = {0}; + BOOL m_SwitchScreen = TRUE; BOOL SendClientClipboard(BOOL fast); VOID UpdateClientClipboard(char *szBuffer, ULONG ulLength); @@ -89,7 +91,7 @@ public: DWORD s_lastThreadId = 0; bool SwitchScreen(); - bool RestartScreen(); + bool RestartScreen(BOOL switchScreen = FALSE); void SwitchToNextWindow(); // 切换到下一个窗口(类似 Alt+Tab) virtual BOOL OnReconnect(); uint64_t m_nReconnectTime = 0; // 重连开始时间 diff --git a/client/ScreenSpy.cpp b/client/ScreenSpy.cpp index a90d313..2a3c310 100644 --- a/client/ScreenSpy.cpp +++ b/client/ScreenSpy.cpp @@ -12,8 +12,9 @@ // Construction/Destruction ////////////////////////////////////////////////////////////////////// -CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL all, int level) : - ScreenCapture(ulbiBitCount, algo, all, level) +CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL all, int level, + RECT rc, BOOL switchScreen) : + ScreenCapture(ulbiBitCount, algo, all, level, rc, switchScreen) { m_GOP = gop; @@ -72,6 +73,15 @@ CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL m_bVirtualPaint = vDesk; m_data.Create(m_hDeskTopDC, m_iScreenX, m_iScreenY, m_ulFullWidth, m_ulFullHeight); + // ROI + int w = m_ROI.right - m_ROI.left, h = m_ROI.bottom - m_ROI.top; + if (w > 0 && h > 0) { + m_nScaleSendWidth = m_BitmapInfor_Send->bmiHeader.biWidth; + m_nScaleSendHeight = m_BitmapInfor_Send->bmiHeader.biHeight; + m_BitmapInfor_Send->bmiHeader.biWidth = w; + m_BitmapInfor_Send->bmiHeader.biHeight = h; + m_BitmapInfor_Send->bmiHeader.biSizeImage = w * h * 4; + } } @@ -115,6 +125,7 @@ LPBYTE CScreenSpy::GetFirstScreenData(ULONG* ulFirstScreenLength) ScanScreen(m_hFullMemDC, m_hDeskTopDC, m_ulFullWidth, m_ulFullHeight); m_RectBuffer[0] = TOKEN_FIRSTSCREEN; LPBYTE bmp = scaleBitmap(m_BmpZoomBuffer, (LPBYTE)m_BitmapData_Full); + memcpy(m_FirstBuffer, bmp, m_BitmapInfor_Send->bmiHeader.biSizeImage); memcpy(1 + m_RectBuffer, bmp, m_BitmapInfor_Send->bmiHeader.biSizeImage); if (m_bAlgorithm == ALGORITHM_GRAY) { ToGray(1 + m_RectBuffer, 1 + m_RectBuffer, m_BitmapInfor_Send->bmiHeader.biSizeImage); diff --git a/client/ScreenSpy.h b/client/ScreenSpy.h index 2da8b5f..eb114cf 100644 --- a/client/ScreenSpy.h +++ b/client/ScreenSpy.h @@ -97,7 +97,8 @@ protected: EnumHwndsPrintData m_data; public: - CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk = FALSE, int gop = DEFAULT_GOP, BOOL all = FALSE, int level = LEVEL_H264_SOFT); + CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk = FALSE, int gop = DEFAULT_GOP, BOOL all = FALSE, + int level = LEVEL_H264_SOFT, RECT rc = {0}, BOOL switchScreen = TRUE); virtual ~CScreenSpy(); diff --git a/common/commands.h b/common/commands.h index 4841a1f..68e5b06 100644 --- a/common/commands.h +++ b/common/commands.h @@ -303,6 +303,7 @@ enum { TOKEN_SCREEN_SIZE, // 屏幕大小 TOKEN_DRIVE_LIST_PLUGIN = 150, // 文件管理(插件) TOKEN_DRAWING_BOARD=151, // 画板 + COMMAND_SCREEN_ROI = 152, // 屏幕区域 TOKEN_DECRYPT = 199, TOKEN_REGEDIT = 200, // 注册表