Files
SimpleRemoter/server/2015Remote/ScreenSpyDlg.cpp
2026-04-19 22:55:21 +02:00

3050 lines
108 KiB
C++
Raw 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.
// ScreenSpyDlg.cpp : 实现文件
//
#include "stdafx.h"
#include "2015Remote.h"
#include "ScreenSpyDlg.h"
#include "afxdialogex.h"
#include <imm.h>
#pragma comment(lib, "imm32.lib")
#include <WinUser.h>
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")
#include "CGridDialog.h"
#include "2015RemoteDlg.h"
#include "CPasswordDlg.h"
#include "CDlgFileSend.h"
#include <file_upload.h>
#include <md5.h>
#include <cstdint> // for uint16_t
#include <vector>
#include "WebService.h"
// 文件接收消息数据结构
struct FileV2MsgData {
std::vector<BYTE> data; // 原始数据
uint64_t transferID;
FileV2MsgData(const BYTE* buf, size_t len, uint64_t tid)
: data(buf, buf + len), transferID(tid) {}
};
// CScreenSpyDlg 对话框
// IDM_* enum 已移至 ScreenSpyDlg.h
IMPLEMENT_DYNAMIC(CScreenSpyDlg, CDialog)
// 算法标识 (与客户端 CursorInfo.h 保持一致)
#define ALGORITHM_GRAY 0
#define ALGORITHM_DIFF 1
#define ALGORITHM_H264 2
#define ALGORITHM_HOME 3
#define ALGORITHM_RGB565 3
#define TIMER_ID 132
// 静态成员变量定义
int CScreenSpyDlg::s_nFastStretch = -1; // -1 表示未初始化
bool CScreenSpyDlg::GetFastStretchMode()
{
if (s_nFastStretch < 0) {
s_nFastStretch = THIS_CFG.GetInt("settings", "FastStretch", 0);
}
return s_nFastStretch != 0;
}
void CScreenSpyDlg::SetFastStretchMode(bool bFast)
{
s_nFastStretch = bFast ? 1 : 0;
THIS_CFG.SetInt("settings", "FastStretch", s_nFastStretch);
}
// RGB565 → BGRA32 转换函数
// 输入: RGB565 像素数据 (每像素 2 字节)
// 输出: BGRA 像素数据 (每像素 4 字节)
// RGB565 格式: RRRRRGGG GGGBBBBB (R:5位, G:6位, B:5位)
inline void ConvertRGB565ToBGRA(const uint16_t* src, BYTE* dst, ULONG pixelCount)
{
for (ULONG i = 0; i < pixelCount; i++, src++, dst += 4) {
uint16_t c = *src;
// 位复制填充低位,还原更精确
BYTE r5 = (c >> 11) & 0x1F;
BYTE g6 = (c >> 5) & 0x3F;
BYTE b5 = c & 0x1F;
dst[2] = (r5 << 3) | (r5 >> 2); // R: 5→8位
dst[1] = (g6 << 2) | (g6 >> 4); // G: 6→8位
dst[0] = (b5 << 3) | (b5 >> 2); // B: 5→8位
dst[3] = 0xFF; // A: 不透明
}
}
#ifdef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "FileUpload_Libx64d.lib")
#pragma comment(lib, "PrivateDesktop_Libx64d.lib")
#else
#pragma comment(lib, "FileUpload_Libx64.lib")
#pragma comment(lib, "PrivateDesktop_Libx64.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "FileUpload_Libd.lib")
#pragma comment(lib, "PrivateDesktop_Libd.lib")
#else
#pragma comment(lib, "FileUpload_Lib.lib")
#pragma comment(lib, "PrivateDesktop_Lib.lib")
#endif
#endif
extern "C" void* x265_api_get_192()
{
return nullptr;
}
extern "C" char* __imp_strtok(char* str, const char* delim)
{
return strtok(str, delim);
}
CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTEXT_OBJECT* ContextObject)
: DialogBase(CScreenSpyDlg::IDD, Parent, IOCPServer, ContextObject, 0)
{
m_bUsingFRP = THIS_CFG.GetInt("frp", "UseFrp", 0);
if (m_bUsingFRP != 0 && m_bUsingFRP != 1)
m_bUsingFRP = 1;
m_pParent = Parent;
m_hFullDC = NULL;
m_hFullMemDC = NULL;
m_BitmapHandle = NULL;
m_lastMouseMove = 0;
m_lastMousePoint = {};
m_pCodec = nullptr;
m_pCodecContext = nullptr;
memset(&m_AVPacket, 0, sizeof(AVPacket));
memset(&m_AVFrame, 0, sizeof(AVFrame));
//创建解码器.
bool succeed = false;
m_pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (m_pCodec) {
m_pCodecContext = avcodec_alloc_context3(m_pCodec);
if (m_pCodecContext) {
succeed = (0 == avcodec_open2(m_pCodecContext, m_pCodec, 0));
}
}
m_FrameID = 0;
// 不在构造函数中禁用 IME改为在 OnInitDialog 中仅禁用此窗口的 IME
CHAR szFullPath[MAX_PATH];
GetSystemDirectory(szFullPath, MAX_PATH);
lstrcat(szFullPath, "\\shell32.dll"); //图标
m_hIcon = ExtractIcon(THIS_APP->m_hInstance, szFullPath, 17);
m_bIsFirst = TRUE;
m_ulHScrollPos = 0;
m_ulVScrollPos = 0;
const ULONG ulBitmapInforLength = sizeof(BITMAPINFOHEADER);
m_BitmapInfor_Full = (BITMAPINFO *) new BYTE[ulBitmapInforLength];
m_ContextObject->InDeCompressedBuffer.CopyBuffer(m_BitmapInfor_Full, ulBitmapInforLength, 1);
m_ContextObject->InDeCompressedBuffer.CopyBuffer(&m_Settings, sizeof(ScreenSettings), 57);
// 解析 clientID (在 BITMAPINFOHEADER 之后,偏移 41)
// 格式: [TOKEN_BITMAPINFO:1][BITMAPINFOHEADER:40][clientID:8][dlgID:8][ScreenSettings:...]
LPBYTE pClientID = m_ContextObject->InDeCompressedBuffer.GetBuffer(41);
if (pClientID) {
m_ClientID = *((uint64_t*)pClientID);
Mprintf("[ScreenSpyDlg] Parsed clientID in constructor: %llu\n", m_ClientID);
}
// 从客户端配置初始化自适应质量状态 (QualityLevel: -2=关闭, -1=自适应, 0-5=具体等级)
m_AdaptiveQuality.startTime = GetTickCount64(); // 记录启动时间
if (m_Settings.QualityLevel == QUALITY_DISABLED) {
m_AdaptiveQuality.enabled = false;
m_AdaptiveQuality.currentLevel = QUALITY_GOOD; // 关闭模式时不使用等级
} else if (m_Settings.QualityLevel == QUALITY_ADAPTIVE) {
m_AdaptiveQuality.enabled = true;
m_AdaptiveQuality.currentLevel = QUALITY_GOOD; // 自适应默认从 Good 开始
} else if (m_Settings.QualityLevel >= 0 && m_Settings.QualityLevel < QUALITY_COUNT) {
m_AdaptiveQuality.enabled = false;
m_AdaptiveQuality.currentLevel = m_Settings.QualityLevel;
}
// 初始化当前分辨率限制关闭模式时为0不限制
if (m_Settings.QualityLevel != QUALITY_DISABLED) {
m_AdaptiveQuality.currentMaxWidth = GetQualityProfile(m_AdaptiveQuality.currentLevel).maxWidth;
} else {
m_AdaptiveQuality.currentMaxWidth = 0;
}
m_bIsCtrl = FALSE;
m_bIsTraceCursor = FALSE;
}
VOID CScreenSpyDlg::SendNext(void)
{
BYTE bToken[32] = { COMMAND_NEXT };
uint64_t dlg = (uint64_t)this;
memcpy(bToken+1, &dlg, sizeof(uint64_t));
// 附加服务端能力标志
uint32_t capabilities = CAP_SCROLL_DETECT; // 支持滚动检测优化
memcpy(bToken + 9, &capabilities, sizeof(uint32_t));
// 附加滚动检测间隔0=禁用, 2=每2帧, ...
int scrollInterval = m_Settings.ScrollDetectInterval;
memcpy(bToken + 9 + sizeof(uint32_t), &scrollInterval, sizeof(int));
m_ContextObject->Send2Client(bToken, sizeof(bToken));
}
CScreenSpyDlg::~CScreenSpyDlg()
{
if (m_BitmapInfor_Full!=NULL) {
delete m_BitmapInfor_Full;
m_BitmapInfor_Full = NULL;
}
::ReleaseDC(m_hWnd, m_hFullDC); //GetDC
::DeleteDC(m_hFullMemDC); //Create匹配内存DC
::DeleteObject(m_BitmapHandle);
if (m_BitmapData_Full!=NULL) {
m_BitmapData_Full = NULL;
}
if (m_pCodecContext) {
avcodec_free_context(&m_pCodecContext);
m_pCodecContext = 0;
}
m_pCodec = 0;
// AVFrame需要清除
av_frame_unref(&m_AVFrame);
// 清理自定义光标
if (m_hCustomCursor) {
DestroyCursor(m_hCustomCursor);
m_hCustomCursor = NULL;
}
// 清理音频播放
StopAudioPlayback();
// 清理所有文件接收对话框
for (auto& pair : m_FileRecvDlgs) {
if (pair.second) {
pair.second->DestroyWindow();
delete pair.second;
}
}
m_FileRecvDlgs.clear();
SAFE_DELETE(m_pToolbar);
SAFE_DELETE(m_pStatusInfoWnd);
}
void CScreenSpyDlg::DoDataExchange(CDataExchange* pDX)
{
__super::DoDataExchange(pDX);
}
// ========== CStatusInfoWnd 实现 ==========
BEGIN_MESSAGE_MAP(CStatusInfoWnd, CWnd)
ON_WM_PAINT()
ON_WM_ERASEBKGND()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEMOVE()
END_MESSAGE_MAP()
BOOL CStatusInfoWnd::Create(CWnd* pParent)
{
DWORD dwStyle = WS_POPUP;
DWORD dwExStyle = WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW;
// 注册窗口类
static LPCTSTR szClassName = _T("StatusInfoWnd");
static bool bRegistered = false;
if (!bRegistered) {
WNDCLASS wc = { 0 };
wc.lpfnWndProc = ::DefWindowProc;
wc.hInstance = AfxGetInstanceHandle();
wc.lpszClassName = szClassName;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
AfxRegisterClass(&wc);
bRegistered = true;
}
if (!CreateEx(dwExStyle, szClassName, _T(""), dwStyle,
CRect(0, 0, 170, 24), pParent, 0))
return FALSE;
SetLayeredWindowAttributes(0, 220, LWA_ALPHA);
LoadSettings();
return TRUE;
}
void CStatusInfoWnd::UpdateInfo(double fps, double kbps, const CString& quality)
{
if (kbps >= 1024)
m_strInfo.Format(_T("%.0f FPS | %.1f MB/s | %s"), fps, kbps / 1024, quality.GetString());
else
m_strInfo.Format(_T("%.0f FPS | %.0f KB/s | %s"), fps, kbps, quality.GetString());
if (m_bVisible)
Invalidate(FALSE);
}
void CStatusInfoWnd::Show()
{
m_bVisible = true;
ShowWindow(SW_SHOWNOACTIVATE);
}
void CStatusInfoWnd::Hide()
{
m_bVisible = false;
ShowWindow(SW_HIDE);
}
void CStatusInfoWnd::SetOpacityLevel(int level)
{
m_nOpacityLevel = level;
BYTE opacity;
switch (level) {
case 1: opacity = 191; break; // 75%
case 2: opacity = 128; break; // 50%
default: opacity = 220; break; // 默认略透明
}
SetLayeredWindowAttributes(0, opacity, LWA_ALPHA);
}
void CStatusInfoWnd::UpdatePosition(const RECT& rcMonitor)
{
int width = 170, height = 24;
int monWidth = rcMonitor.right - rcMonitor.left;
int x, y;
if (m_bHasCustomPosition) {
x = rcMonitor.left + (int)(monWidth * m_dOffsetXRatio) - width / 2;
y = rcMonitor.top + m_nOffsetY;
x = max((int)rcMonitor.left, min(x, (int)rcMonitor.right - width));
y = max((int)rcMonitor.top, min(y, (int)rcMonitor.bottom - height));
} else {
x = rcMonitor.left + (monWidth - width) / 2;
y = rcMonitor.top + 50;
}
SetWindowPos(&wndTopMost, x, y, width, height, SWP_NOACTIVATE);
}
void CStatusInfoWnd::LoadSettings()
{
m_bHasCustomPosition = THIS_CFG.GetInt("statusinfo", "HasCustomPos", 0) != 0;
if (m_bHasCustomPosition) {
m_dOffsetXRatio = THIS_CFG.GetDouble("statusinfo", "OffsetXRatio", 0.5);
m_nOffsetY = THIS_CFG.GetInt("statusinfo", "OffsetY", 50);
}
}
void CStatusInfoWnd::SaveSettings()
{
THIS_CFG.SetInt("statusinfo", "HasCustomPos", m_bHasCustomPosition ? 1 : 0);
THIS_CFG.SetDouble("statusinfo", "OffsetXRatio", m_dOffsetXRatio);
THIS_CFG.SetInt("statusinfo", "OffsetY", m_nOffsetY);
}
// 检查父窗口是否处于控制模式
bool CStatusInfoWnd::IsParentInControlMode()
{
CScreenSpyDlg* pParent = (CScreenSpyDlg*)GetParent();
return pParent && pParent->m_bIsCtrl;
}
void CStatusInfoWnd::OnLButtonDown(UINT nFlags, CPoint point)
{
// 控制模式下不处理拖拽,让消息传递到父窗口
if (IsParentInControlMode()) {
// 转换为屏幕坐标后发送给父窗口
CPoint ptScreen = point;
ClientToScreen(&ptScreen);
CWnd* pParent = GetParent();
if (pParent) {
pParent->ScreenToClient(&ptScreen);
pParent->PostMessage(WM_LBUTTONDOWN, nFlags, MAKELPARAM(ptScreen.x, ptScreen.y));
}
return;
}
m_bDragging = true;
m_ptDragStart = point;
SetCapture();
}
void CStatusInfoWnd::OnLButtonUp(UINT nFlags, CPoint point)
{
if (IsParentInControlMode()) {
CPoint ptScreen = point;
ClientToScreen(&ptScreen);
CWnd* pParent = GetParent();
if (pParent) {
pParent->ScreenToClient(&ptScreen);
pParent->PostMessage(WM_LBUTTONUP, nFlags, MAKELPARAM(ptScreen.x, ptScreen.y));
}
return;
}
if (m_bDragging) {
m_bDragging = false;
ReleaseCapture();
CWnd* pParent = GetParent();
if (pParent) {
HMONITOR hMon = MonitorFromWindow(pParent->GetSafeHwnd(), MONITOR_DEFAULTTONEAREST);
MONITORINFO mi = { sizeof(mi) };
if (GetMonitorInfo(hMon, &mi)) {
CRect rcWnd;
GetWindowRect(&rcWnd);
int monWidth = mi.rcMonitor.right - mi.rcMonitor.left;
int wndCenterX = rcWnd.left + rcWnd.Width() / 2;
m_dOffsetXRatio = (double)(wndCenterX - mi.rcMonitor.left) / monWidth;
m_nOffsetY = rcWnd.top - mi.rcMonitor.top;
m_bHasCustomPosition = true;
SaveSettings();
}
}
}
}
void CStatusInfoWnd::OnMouseMove(UINT nFlags, CPoint point)
{
if (IsParentInControlMode()) {
CPoint ptScreen = point;
ClientToScreen(&ptScreen);
CWnd* pParent = GetParent();
if (pParent) {
pParent->ScreenToClient(&ptScreen);
pParent->PostMessage(WM_MOUSEMOVE, nFlags, MAKELPARAM(ptScreen.x, ptScreen.y));
}
return;
}
if (m_bDragging) {
CRect rcWnd;
GetWindowRect(&rcWnd);
int dx = point.x - m_ptDragStart.x;
int dy = point.y - m_ptDragStart.y;
SetWindowPos(&wndTopMost, rcWnd.left + dx, rcWnd.top + dy,
0, 0, SWP_NOSIZE | SWP_NOACTIVATE);
}
}
void CStatusInfoWnd::OnPaint()
{
CPaintDC dc(this);
CRect rc;
GetClientRect(&rc);
// 背景
dc.FillSolidRect(rc, RGB(40, 40, 40));
// 文字
dc.SetBkMode(TRANSPARENT);
dc.SetTextColor(RGB(220, 220, 220));
CFont font;
font.CreatePointFont(90, _T("Segoe UI"));
CFont* pOldFont = dc.SelectObject(&font);
dc.DrawText(m_strInfo, rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
dc.SelectObject(pOldFont);
}
BOOL CStatusInfoWnd::OnEraseBkgnd(CDC* pDC)
{
return TRUE;
}
// ========== CScreenSpyDlg 实现 ==========
BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
ON_WM_CLOSE()
ON_WM_PAINT()
ON_WM_SETCURSOR()
ON_WM_SYSCOMMAND()
ON_WM_HSCROLL()
ON_WM_VSCROLL()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEWHEEL()
ON_WM_MOUSEMOVE()
ON_WM_MOUSELEAVE()
ON_WM_KILLFOCUS()
ON_WM_SIZE()
ON_WM_LBUTTONDBLCLK()
ON_WM_ACTIVATE()
ON_WM_TIMER()
ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen)
ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo)
ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo)
ON_MESSAGE(WM_DISCONNECT, &CScreenSpyDlg::OnDisconnect)
ON_MESSAGE(MM_WOM_DONE, &CScreenSpyDlg::OnWaveOutDone)
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
ON_WM_DROPFILES()
END_MESSAGE_MAP()
// CScreenSpyDlg 消息处理程序
void CScreenSpyDlg::OnLButtonDblClk(UINT nFlags, CPoint point)
{
if (!m_bIsCtrl) {
CWnd* parent = GetParent();
if (parent) {
// 通知父对话框,传递点击点
CPoint ptScreen = point;
ClientToScreen(&ptScreen);
GetParent()->ScreenToClient(&ptScreen);
GetParent()->SendMessage(WM_LBUTTONDBLCLK, nFlags, MAKELPARAM(ptScreen.x, ptScreen.y));
}
}
__super::OnLButtonDblClk(nFlags, point);
}
void CScreenSpyDlg::PrepareDrawing(const LPBITMAPINFO bmp)
{
if (m_hFullDC) ::ReleaseDC(m_hWnd, m_hFullDC);
if (m_hFullMemDC) ::DeleteDC(m_hFullMemDC);
if (m_BitmapHandle) ::DeleteObject(m_BitmapHandle);
m_BitmapData_Full = NULL;
CString strString;
strString.FormatL("%s - 远程桌面控制 %d×%d", m_IPAddress, bmp->bmiHeader.biWidth, bmp->bmiHeader.biHeight);
SetWindowText(strString);
uint64_t dlg = (uint64_t)this;
Mprintf("%s [对话框ID: %llu]\n", strString.GetString(), dlg);
m_hFullDC = ::GetDC(m_hWnd);
SetStretchBltMode(m_hFullDC, GetFastStretchMode() ? COLORONCOLOR : HALFTONE);
if (!GetFastStretchMode()) SetBrushOrgEx(m_hFullDC, 0, 0, NULL);
m_hFullMemDC = CreateCompatibleDC(m_hFullDC);
m_BitmapHandle = CreateDIBSection(m_hFullDC, bmp, DIB_RGB_COLORS, &m_BitmapData_Full, NULL, NULL);
SelectObject(m_hFullMemDC, m_BitmapHandle);
// 仅在滚动条已可见时更新范围(不会闪现,只是刷新范围值)
// 全屏或自适应模式下不调用,避免隐式添加 WS_HSCROLL/WS_VSCROLL 导致闪现
if (!m_Settings.FullScreen && !m_bAdaptiveSize) {
SetScrollRange(SB_HORZ, 0, bmp->bmiHeader.biWidth);
SetScrollRange(SB_VERT, 0, bmp->bmiHeader.biHeight);
}
GetClientRect(&m_CRect);
m_wZoom = ((double)bmp->bmiHeader.biWidth) / ((double)(m_CRect.Width()));
m_hZoom = ((double)bmp->bmiHeader.biHeight) / ((double)(m_CRect.Height()));
}
BOOL CScreenSpyDlg::OnInitDialog()
{
__super::OnInitDialog();
SetIcon(m_hIcon,FALSE);
// 获取默认 IME 上下文ImmAssociateContext 返回之前关联的上下文)
// 先禁用再恢复,以获取原始上下文句柄
m_hOldIMC = ImmAssociateContext(m_hWnd, NULL);
if (m_hOldIMC) {
ImmAssociateContext(m_hWnd, m_hOldIMC); // 立即恢复
}
DragAcceptFiles(TRUE);
ChangeWindowMessageFilter(WM_DROPFILES, MSGFLT_ADD);
ChangeWindowMessageFilter(0x0049, MSGFLT_ADD);
PrepareDrawing(m_BitmapInfor_Full);
CMenu* SysMenu = GetSystemMenu(FALSE);
if (SysMenu != NULL) {
SysMenu->AppendMenuSeparator(MF_SEPARATOR);
SysMenu->AppendMenuL(MF_STRING, IDM_CONTROL, "控制屏幕(&Y)");
SysMenu->AppendMenuL(MF_STRING, IDM_FULLSCREEN, "全屏(&F)");
SysMenu->AppendMenuL(MF_STRING, IDM_REMOTE_CURSOR, "使用远程光标(&C)");
SysMenu->AppendMenuL(MF_STRING, IDM_ADAPTIVE_SIZE, "自适应窗口大小(&A)");
SysMenu->AppendMenuL(MF_STRING, IDM_TRACE_CURSOR, "跟踪被控端鼠标(&T)");
SysMenu->AppendMenuL(MF_STRING, IDM_BLOCK_INPUT, "锁定被控端鼠标和键盘(&L)");
SysMenu->AppendMenuL(MF_STRING, IDM_RESTORE_CONSOLE, "RDP会话归位(&R)");
// 只在虚拟桌面模式下显示重置选项
if (m_Settings.ScreenType == USING_VIRTUAL) {
SysMenu->AppendMenuL(MF_STRING, IDM_RESET_VIRTUAL_DESKTOP, "重置虚拟桌面(&V)");
}
SysMenu->AppendMenuSeparator(MF_SEPARATOR);
SysMenu->AppendMenuL(MF_STRING, IDM_SAVEDIB, "保存快照(&S)");
SysMenu->AppendMenuL(MF_STRING, IDM_SAVEAVI, _T("录像(MJPEG)"));
SysMenu->AppendMenuL(MF_STRING, IDM_SAVEAVI_H264, _T("录像(H264)"));
SysMenu->AppendMenuL(MF_STRING, IDM_GET_CLIPBOARD, "获取剪贴板(&R)");
SysMenu->AppendMenuL(MF_STRING, IDM_SET_CLIPBOARD, "设置剪贴板(&L)");
SysMenu->AppendMenuSeparator(MF_SEPARATOR);
SysMenu->AppendMenuL(MF_STRING, IDM_SWITCHSCREEN, "切换显示器(&1)");
SysMenu->AppendMenuL(MF_STRING, IDM_MULTITHREAD_COMPRESS, "多线程压缩(&2)");
SysMenu->AppendMenuL(MF_STRING, IDM_ORIGINAL_SIZE, "原始分辨率(&3)");
SysMenu->AppendMenuL(MF_STRING, IDM_SCREEN_1080P, "限制为1080P(&4)");
SysMenu->AppendMenuL(MF_STRING, IDM_ENABLE_SSE2, "启用SSE2指令集(&5)");
SysMenu->AppendMenuL(MF_STRING, IDM_FAST_STRETCH, "服务端快速缩放(降低CPU)(&6)");
SysMenu->AppendMenuL(MF_STRING, IDM_CUSTOM_CURSOR, "使用自定义光标(&7)");
SysMenu->AppendMenuSeparator(MF_SEPARATOR);
SysMenu->CheckMenuItem(IDM_FULLSCREEN, m_Settings.FullScreen ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_REMOTE_CURSOR, m_Settings.RemoteCursor ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_ENABLE_SSE2, m_Settings.CpuSpeedup == 1 ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_FAST_STRETCH, GetFastStretchMode() ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_bUseCustomCursor ? MF_CHECKED : MF_UNCHECKED);
CMenu fpsMenu;
if (fpsMenu.CreatePopupMenu()) {
fpsMenu.AppendMenuL(MF_STRING, IDM_FPS_10, "最大帧率FPS:10");
fpsMenu.AppendMenuL(MF_STRING, IDM_FPS_15, "最大帧率FPS:15");
fpsMenu.AppendMenuL(MF_STRING, IDM_FPS_20, "最大帧率FPS:20");
fpsMenu.AppendMenuL(MF_STRING, IDM_FPS_25, "最大帧率FPS:25");
fpsMenu.AppendMenuL(MF_STRING, IDM_FPS_30, "最大帧率FPS:30");
fpsMenu.AppendMenuL(MF_STRING, IDM_FPS_UNLIMITED, "最大帧率无限制");
SysMenu->AppendMenuL(MF_STRING | MF_POPUP, (UINT_PTR)fpsMenu.Detach(), _T("帧率设置"));
}
// 滚动检测间隔菜单
// 滚动检测菜单(用户友好描述)
CMenu scrollMenu;
if (scrollMenu.CreatePopupMenu()) {
scrollMenu.AppendMenuL(MF_STRING, IDM_SCROLL_DETECT_OFF, "关闭(局域网)");
scrollMenu.AppendMenuL(MF_STRING, IDM_SCROLL_DETECT_2, "跨网推荐(最省流量)");
scrollMenu.AppendMenuL(MF_STRING, IDM_SCROLL_DETECT_4, "标准模式");
scrollMenu.AppendMenuL(MF_STRING, IDM_SCROLL_DETECT_8, "低频模式(省CPU)");
SysMenu->AppendMenuL(MF_STRING | MF_POPUP, (UINT_PTR)scrollMenu.Detach(), _T("滚动优化"));
}
// 屏幕质量子菜单
CMenu qualityMenu;
if (qualityMenu.CreatePopupMenu()) {
qualityMenu.AppendMenuL(MF_STRING, IDM_QUALITY_OFF, "关闭(&O)");
qualityMenu.AppendMenuL(MF_STRING, IDM_ADAPTIVE_QUALITY, "自适应(&A)");
qualityMenu.AppendMenuSeparator(MF_SEPARATOR);
qualityMenu.AppendMenuL(MF_STRING, IDM_QUALITY_ULTRA, "Ultra (25FPS, DIFF - 企业级/高速局域网)");
qualityMenu.AppendMenuL(MF_STRING, IDM_QUALITY_HIGH, "High (20FPS, RGB565 - 家庭级/标准局域网)");
qualityMenu.AppendMenuL(MF_STRING, IDM_QUALITY_GOOD, "Good (20FPS, H264) - 跨网/默认办公标配");
qualityMenu.AppendMenuL(MF_STRING, IDM_QUALITY_MEDIUM, "Medium (15FPS, H264) - 跨国/跨境远控");
qualityMenu.AppendMenuL(MF_STRING, IDM_QUALITY_LOW, "Low (12FPS, H264) - 洲际/弱网远控");
qualityMenu.AppendMenuL(MF_STRING, IDM_QUALITY_MINIMAL, "Minimal (8FPS, H264) - 极差/极低带宽");
SysMenu->AppendMenuL(MF_STRING | MF_POPUP, (UINT_PTR)qualityMenu.Detach(), _T("屏幕质量(&Q)"));
}
// 音频菜单项
SysMenu->AppendMenuL(MF_STRING, IDM_AUDIO_TOGGLE, "系统音频(&U)");
SysMenu->CheckMenuItem(IDM_AUDIO_TOGGLE, m_Settings.AudioEnabled ? MF_CHECKED : MF_UNCHECKED);
// 初始化勾选状态
UpdateQualityMenuCheck(SysMenu);
BOOL all = THIS_CFG.GetInt("settings", "MultiScreen", TRUE);
SysMenu->EnableMenuItem(IDM_SWITCHSCREEN, all ? MF_ENABLED : MF_GRAYED);
SysMenu->CheckMenuItem(IDM_MULTITHREAD_COMPRESS, m_Settings.CompressThread ? MF_CHECKED : MF_UNCHECKED);
if (m_Settings.ScreenStrategy == 0) {
SysMenu->CheckMenuItem(IDM_SCREEN_1080P, MF_CHECKED);
SysMenu->CheckMenuItem(IDM_ORIGINAL_SIZE, MF_UNCHECKED);
} else if (m_Settings.ScreenStrategy == 1) {
SysMenu->CheckMenuItem(IDM_SCREEN_1080P, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_ORIGINAL_SIZE, MF_CHECKED);
}
int fpsIndex = IDM_FPS_10 + (m_Settings.MaxFPS - 10)/5;
for (int i = IDM_FPS_10; i <= IDM_FPS_UNLIMITED; i++) {
SysMenu->CheckMenuItem(i, MF_UNCHECKED);
}
SysMenu->CheckMenuItem(fpsIndex, MF_CHECKED);
// 设置滚动检测间隔选中状态
SysMenu->CheckMenuItem(IDM_SCROLL_DETECT_OFF, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_SCROLL_DETECT_2, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_SCROLL_DETECT_4, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_SCROLL_DETECT_8, MF_UNCHECKED);
int scrollInterval = m_Settings.ScrollDetectInterval;
int scrollMenuID = scrollInterval <= 0 ? IDM_SCROLL_DETECT_OFF :
scrollInterval <= 2 ? IDM_SCROLL_DETECT_2 :
scrollInterval <= 4 ? IDM_SCROLL_DETECT_4 : IDM_SCROLL_DETECT_8;
SysMenu->CheckMenuItem(scrollMenuID, MF_CHECKED);
}
m_bIsCtrl = m_Settings.ScreenType == USING_VIRTUAL;
// 根据初始控制状态设置 IME
if (m_bIsCtrl) {
ImmAssociateContext(m_hWnd, NULL); // 控制模式:禁用 IME
}
m_bIsTraceCursor = FALSE; //不是跟踪
m_ClientCursorPos.x = 0;
m_ClientCursorPos.y = 0;
m_bCursorIndex = 0;
m_hRemoteCursor = LoadCursor(NULL, IDC_ARROW);
ICONINFO CursorInfo;
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
// 设置合理的"正常"窗口大小,显示在主程序所在的显示器上
RECT rcMon = { 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN) };
CWnd* pMain = AfxGetMainWnd();
if (pMain) {
HMONITOR hMon = MonitorFromWindow(pMain->GetSafeHwnd(), MONITOR_DEFAULTTONEAREST);
MONITORINFO mi = { sizeof(mi) };
if (GetMonitorInfo(hMon, &mi))
rcMon = mi.rcWork;
}
int monW = rcMon.right - rcMon.left;
int monH = rcMon.bottom - rcMon.top;
int normalWidth = (int)(monW * 0.382);
int normalHeight = (int)(monH * 0.382);
int normalX = rcMon.left + (monW - normalWidth) / 2;
int normalY = rcMon.top + (monH - normalHeight) / 2;
// 使用 WINDOWPLACEMENT 确保 rcNormalPosition 被正确设置
WINDOWPLACEMENT wp = { sizeof(WINDOWPLACEMENT) };
GetWindowPlacement(&wp);
wp.rcNormalPosition.left = normalX;
wp.rcNormalPosition.top = normalY;
wp.rcNormalPosition.right = normalX + normalWidth;
wp.rcNormalPosition.bottom = normalY + normalHeight;
wp.showCmd = SW_MAXIMIZE;
SetWindowPlacement(&wp);
// 同时初始化 m_struOldWndpl供全屏退出时使用
m_struOldWndpl = wp;
m_Settings.FullScreen ? EnterFullScreen() : LeaveFullScreen();
// 启动传输速率更新定时器 (1秒)
SetTimer(4, 1000, NULL);
// 下发质量配置表(让客户端可以持久化保存)
{
BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT];
profileCmd[0] = CMD_QUALITY_PROFILES;
for (int i = 0; i < QUALITY_COUNT; i++) {
memcpy(profileCmd + 1 + i * sizeof(QualityProfile),
&GetQualityProfile(i), sizeof(QualityProfile));
}
m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd));
}
// 发送初始质量配置给客户端
if (m_Settings.QualityLevel == QUALITY_DISABLED) {
// 关闭模式:不发送任何质量命令,保持客户端原有设置
} else {
// 自适应或固定等级模式:发送质量等级和分辨率
BYTE cmd[4] = { CMD_QUALITY_LEVEL, (BYTE)m_AdaptiveQuality.currentLevel, 0 };
m_ContextObject->Send2Client(cmd, sizeof(cmd));
// 始终发送 CMD_SCREEN_SIZE让客户端同步分辨率策略
// maxWidth=0 表示使用默认分辨率1080p 限制)
BYTE sizeCmd[16] = { CMD_SCREEN_SIZE, 2 }; // strategy=2 表示自适应质量
memcpy(sizeCmd + 2, &m_AdaptiveQuality.currentMaxWidth, sizeof(int));
m_ContextObject->Send2Client(sizeCmd, 10);
}
SendNext();
// 通知主窗口:设置为活动的远程桌面会话(用于 Ctrl+V 文件接收)
if (pMain)
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
// 注册屏幕上下文到 WebService用于 Web 端鼠标/键盘控制)
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
return TRUE;
}
VOID CScreenSpyDlg::OnClose()
{
// 注销屏幕上下文Web 端控制)
WebService().UnregisterScreenContext(m_ClientID);
m_bIsClosed = true;
m_bIsCtrl = FALSE;
CWnd* pMain = AfxGetMainWnd();
if (pMain)
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)nullptr, 0);
KillTimer(1);
KillTimer(2);
KillTimer(3);
KillTimer(4);
if (!m_aviFile.IsEmpty()) {
KillTimer(TIMER_ID);
m_aviFile = "";
m_aviStream.Close();
}
bool needCancel = (ShouldReconnect() && SayByeBye());
// 恢复鼠标状态
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)LoadCursor(NULL, IDC_ARROW));
// 通知父窗口
CWnd* parent = GetParent();
if (parent)
parent->SendMessage(WM_CHILD_CLOSED, (WPARAM)this, 0);
extern CMy2015RemoteDlg *g_2015RemoteDlg;
if(g_2015RemoteDlg)
g_2015RemoteDlg->RemoveRemoteWindow(GetSafeHwnd());
// 等待数据处理完毕
if (IsProcessing()) {
m_bHide = true;
ShowWindow(SW_HIDE);
if (needCancel) { BeginWaitCursor(); Sleep(500); CancelIO(); EndWaitCursor(); }
return;
}
if (needCancel) { BeginWaitCursor(); Sleep(500); CancelIO(); EndWaitCursor(); }
DialogBase::OnClose();
}
afx_msg LRESULT CScreenSpyDlg::OnDisconnect(WPARAM wParam, LPARAM lParam)
{
m_bConnected = FALSE;
m_nDisconnectTime = GetTickCount64();
// Close the dialog if reconnect not succeed in 15 seconds
SetTimer(2, 15000, NULL);
SetTimer(3, 3000, NULL);
PostMessage(WM_PAINT);
return S_OK;
}
// 处理文件块接收(主线程)
LRESULT CScreenSpyDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam)
{
FileV2MsgData* msgData = (FileV2MsgData*)wParam;
if (!msgData) return 0;
BYTE* szBuffer = msgData->data.data();
size_t len = msgData->data.size();
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
uint64_t transferID = msgData->transferID;
// 创建或获取进度对话框(按 transferID 管理)
CDlgFileSend*& dlg = m_FileRecvDlgs[transferID];
if (dlg == nullptr) {
dlg = new CDlgFileSend(m_pParent, m_ContextObject->GetServer(), m_ContextObject, FALSE);
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
dlg->SetWindowTextA(_TR("接收文件"));
dlg->ShowWindow(SW_SHOW);
dlg->m_bKeepConnection = TRUE; // 不断开连接
}
// 接收文件
std::string hash = GetPwdHash(), hmac = GetHMAC(100);
int n = RecvFileChunkV2((char*)szBuffer, len, nullptr, nullptr, hash, hmac, 0);
if (n) {
Mprintf("[ScreenSpy] RecvFileChunkV2 failed: %d\n", n);
}
// 更新进度
BYTE* name = szBuffer + sizeof(FileChunkPacketV2);
dlg->UpdateProgress(CString((char*)name, (int)pkt->nameLength), FileProgressInfo(pkt));
// 最后一个文件的最后一个包
if (pkt->fileIndex + 1 == pkt->totalFiles &&
pkt->offset + pkt->dataLength >= pkt->fileSize) {
// 等待 COMMAND_FILE_COMPLETE_V2 来最终确认
// dlg->FinishFileSend(TRUE);
// m_FileRecvDlgs.erase(transferID);
}
delete msgData;
return 0;
}
// 处理文件完成校验(主线程)
LRESULT CScreenSpyDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam)
{
FileV2MsgData* msgData = (FileV2MsgData*)wParam;
if (!msgData) return 0;
BYTE* szBuffer = msgData->data.data();
size_t len = msgData->data.size();
uint64_t transferID = msgData->transferID;
// 本地校验
bool verifyOk = HandleFileCompleteV2((const char*)szBuffer, len, 0);
Mprintf("[ScreenSpy] 文件校验%s\n", verifyOk ? "通过" : "失败");
// 关闭进度对话框
auto it = m_FileRecvDlgs.find(transferID);
if (it != m_FileRecvDlgs.end()) {
it->second->FinishFileSend(verifyOk);
m_FileRecvDlgs.erase(it);
}
delete msgData;
return 0;
}
VOID CScreenSpyDlg::OnReceiveComplete()
{
if (m_bIsClosed) return;
assert (m_ContextObject);
auto cmd = m_ContextObject->InDeCompressedBuffer.GetBYTE(0);
LPBYTE szBuffer = m_ContextObject->InDeCompressedBuffer.GetBuffer();
unsigned len = m_ContextObject->InDeCompressedBuffer.GetBufferLen();
m_ulBytesThisSecond += len; // 累计传输字节
switch(cmd) {
case COMMAND_GET_FOLDER: {
std::string folder;
if (GetCurrentFolderPath(folder)) {
// 发送目录并准备接收文件
std::string files(szBuffer + 1, szBuffer + len);
int len = 1 + folder.length() + files.length() + 1;
BYTE* cmd = new BYTE[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;
m_ContextObject->Send2Client(cmd, len);
SAFE_DELETE_ARRAY(cmd);
}
break;
}
case TOKEN_FIRSTSCREEN: {
DrawFirstScreen();
m_ulFramesThisSecond++;
break;
}
case TOKEN_NEXTSCREEN: {
DrawNextScreenDiff(false);
m_ulFramesThisSecond++;
break;
}
case TOKEN_KEYFRAME: {
if (!m_bIsFirst) {
DrawNextScreenDiff(true);
}
m_ulFramesThisSecond++;
break;
}
case TOKEN_SCROLL_FRAME: {
if (!m_bIsFirst) {
DrawScrollFrame();
}
m_ulFramesThisSecond++;
break;
}
case TOKEN_CLIPBOARD_TEXT: {
Buffer str = m_ContextObject->InDeCompressedBuffer.GetMyBuffer(1);
UpdateServerClipboard(str.c_str(), str.length());
break;
}
case TOKEN_BITMAPINFO: {
SAFE_DELETE(m_BitmapInfor_Full);
m_bIsFirst = TRUE;
const ULONG ulBitmapInforLength = sizeof(BITMAPINFOHEADER);
m_BitmapInfor_Full = (BITMAPINFO*) new BYTE[ulBitmapInforLength];
m_ContextObject->InDeCompressedBuffer.CopyBuffer(m_BitmapInfor_Full, ulBitmapInforLength, 1);
if (len >= 1 + sizeof(BITMAPINFOHEADER) + sizeof(uint64_t)) {
m_ClientID = *((uint64_t*)(m_ContextObject->InDeCompressedBuffer.GetBuffer(1 + sizeof(BITMAPINFOHEADER))));
}
PrepareDrawing(m_BitmapInfor_Full);
// 分辨率切换完成,允许解码
m_bResolutionChanging = false;
// Notify web clients of resolution change
if (WebService().IsRunning()) {
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
WebService().NotifyResolutionChange(m_ClientID, width, height);
// Hide window if this session was triggered by web client (and hiding is enabled)
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
m_bHide = true;
ShowWindow(SW_HIDE);
Mprintf("[ScreenSpyDlg] Web-triggered session, hiding window for device %llu\n", m_ClientID);
}
}
break;
}
case COMMAND_FILE_QUERY_RESUME: {
// V2 续传查询 - 根据 dstClientID 决定路由
FileQueryResumeV2* query = (FileQueryResumeV2*)szBuffer;
if (query->dstClientID == 0) {
// 目标是主控端 - 本地处理
auto response = HandleResumeQuery((const char*)szBuffer, len);
if (!response.empty()) {
m_ContextObject->Send2Client((LPBYTE)response.data(), (int)response.size());
Mprintf("[ScreenSpy] 已响应续传查询: %zu 字节\n", response.size());
}
} else {
// C2C - 转发到目标客户端的主连接
context* dstCtx = m_pParent->FindHost(query->dstClientID);
if (dstCtx) {
dstCtx->Send2Client(szBuffer, len);
Mprintf("[ScreenSpy] 转发续传查询: -> %llu\n", query->dstClientID);
} else {
Mprintf("[ScreenSpy] 续传查询目标不在线: %llu\n", query->dstClientID);
}
}
break;
}
case COMMAND_C2C_TEXT: {
// C2C 文本剪贴板: [cmd:1][dstClientID:8][textLen:4][text:N]
if (len < 13) break;
uint64_t dstClientID;
uint32_t textLen;
memcpy(&dstClientID, szBuffer + 1, 8);
memcpy(&textLen, szBuffer + 9, 4);
Mprintf("[ScreenSpy] C2C 文本转发: -> %llu (%u 字节)\n", dstClientID, textLen);
// 转发到目标客户端的主连接
context* dstCtx = m_pParent->FindHost(dstClientID);
if (dstCtx) {
dstCtx->Send2Client(szBuffer, len);
} else {
Mprintf("[ScreenSpy] 文本目标不在线: %llu\n", dstClientID);
}
break;
}
case COMMAND_SEND_FILE_V2: {
// V2 文件传输
if (len < sizeof(FileChunkPacketV2)) break;
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
if (pkt->dstClientID == 0) {
// 目标是服务端:本地接收
// 使用 PostMessage 将数据转发到主线程处理,避免在工作线程中操作 UI
FileV2MsgData* msgData = new FileV2MsgData(szBuffer, len, pkt->transferID);
PostMessage(WM_RECVFILEV2_CHUNK, (WPARAM)msgData, 0);
} else {
// C2C转发到目标客户端
context* dstCtx = m_pParent->FindHost(pkt->dstClientID);
if (dstCtx) {
dstCtx->Send2Client(szBuffer, len);
} else {
Mprintf("[ScreenSpy] C2C 目标不在线: %llu\n", pkt->dstClientID);
}
}
break;
}
case COMMAND_FILE_COMPLETE_V2: {
// V2 文件完成校验
if (len < sizeof(FileCompletePacketV2)) break;
FileCompletePacketV2* pkt = (FileCompletePacketV2*)szBuffer;
if (pkt->dstClientID == 0) {
// 目标是服务端:使用 PostMessage 转发到主线程处理
FileV2MsgData* msgData = new FileV2MsgData(szBuffer, len, pkt->transferID);
PostMessage(WM_RECVFILEV2_COMPLETE, (WPARAM)msgData, 0);
} else {
// C2C转发到目标客户端
context* dstCtx = m_pParent->FindHost(pkt->dstClientID);
if (dstCtx) {
dstCtx->Send2Client(szBuffer, len);
Mprintf("[ScreenSpy] 转发校验包: -> %llu, transferID=%llu\n",
pkt->dstClientID, pkt->transferID);
} else {
Mprintf("[ScreenSpy] 校验包目标不在线: %llu\n", pkt->dstClientID);
}
}
break;
}
case COMMAND_FILE_RESUME: {
// V2 断点续传控制 - 转发
// 注意:有两种格式的包共用此命令:
// - FileResumePacketV2 (49 bytes): 单文件续传请求/响应
// - FileResumeResponseV2 (23 bytes): 批量续传查询响应
// 需要根据包大小区分
if (len >= sizeof(FileResumePacketV2)) {
// 大包: FileResumePacketV2
FileResumePacketV2* pkt = (FileResumePacketV2*)szBuffer;
context* dstCtx = m_pParent->FindHost(pkt->dstClientID);
if (dstCtx) {
dstCtx->Send2Client(szBuffer, len);
Mprintf("[ScreenSpy] 转发续传包: -> %llu, transferID=%llu\n",
pkt->dstClientID, pkt->transferID);
}
} else if (len >= sizeof(FileResumeResponseV2)) {
// 小包: FileResumeResponseV2 (批量响应)
// 注意:响应要发回给 srcClientID原始发送方
FileResumeResponseV2* resp = (FileResumeResponseV2*)szBuffer;
context* srcCtx = m_pParent->FindHost(resp->srcClientID);
if (srcCtx) {
srcCtx->Send2Client(szBuffer, len);
Mprintf("[ScreenSpy] 转发续传响应: -> srcClient=%llu, %u 个文件\n",
resp->srcClientID, resp->fileCount);
} else {
Mprintf("[ScreenSpy] 续传响应目标不在线: %llu\n", resp->srcClientID);
}
}
break;
}
case CMD_CURSOR_IMAGE: {
// 自定义光标图像: [cmd:1][hash:4][hotX:2][hotY:2][w:1][h:1][BGRA:w*h*4]
if (len < 11) break;
DWORD hash = *(DWORD*)(szBuffer + 1);
if (hash == m_dwCustomCursorHash && m_hCustomCursor) break; // 相同光标且已创建,忽略
WORD hotX = *(WORD*)(szBuffer + 5);
WORD hotY = *(WORD*)(szBuffer + 7);
BYTE width = szBuffer[9];
BYTE height = szBuffer[10];
// 检查尺寸有效性
if (width == 0 || height == 0 || width > 64 || height > 64) break;
LPBYTE bgraData = szBuffer + 11;
DWORD dataSize = width * height * 4;
if (len < 11 + dataSize) break; // 数据不完整
// 创建位图
HDC hScreenDC = ::GetDC(NULL);
if (!hScreenDC) break;
HDC hMemDC = ::CreateCompatibleDC(hScreenDC);
if (!hMemDC) {
::ReleaseDC(NULL, hScreenDC);
break;
}
BITMAPINFO bmi = { 0 };
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -height; // 负数表示从上到下
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* pColorBits = NULL;
HBITMAP hColorBmp = CreateDIBSection(hScreenDC, &bmi, DIB_RGB_COLORS, &pColorBits, NULL, 0);
if (!hColorBmp || !pColorBits) {
::DeleteDC(hMemDC);
::ReleaseDC(NULL, hScreenDC);
break;
}
memcpy(pColorBits, bgraData, dataSize);
// 创建掩码位图1bpp行宽度 WORD 对齐)
int maskStride = ((width + 15) / 16) * 2; // 每行字节数16位对齐
int maskSize = maskStride * height;
BYTE* maskBits = new BYTE[maskSize];
memset(maskBits, 0, maskSize);
// 根据 Alpha 通道生成掩码(内存操作,比 SetPixel 快 100 倍)
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
BYTE alpha = bgraData[(y * width + x) * 4 + 3];
if (alpha < 128) {
// 透明像素:掩码位=1
maskBits[y * maskStride + x / 8] |= (0x80 >> (x % 8));
}
}
}
HBITMAP hMaskBmp = ::CreateBitmap(width, height, 1, 1, maskBits);
delete[] maskBits;
if (!hMaskBmp) {
::DeleteObject(hColorBmp);
::DeleteDC(hMemDC);
::ReleaseDC(NULL, hScreenDC);
break;
}
// 创建光标
ICONINFO iconInfo = { 0 };
iconInfo.fIcon = FALSE;
iconInfo.xHotspot = hotX;
iconInfo.yHotspot = hotY;
iconInfo.hbmMask = hMaskBmp;
iconInfo.hbmColor = hColorBmp;
HCURSOR hNewCursor = CreateIconIndirect(&iconInfo);
// 清理位图CreateIconIndirect 会复制)
::DeleteObject(hColorBmp);
::DeleteObject(hMaskBmp);
::DeleteDC(hMemDC);
::ReleaseDC(NULL, hScreenDC);
// 只有成功创建才更新
if (hNewCursor) {
if (m_hCustomCursor) {
DestroyCursor(m_hCustomCursor);
}
m_hCustomCursor = hNewCursor;
m_dwCustomCursorHash = hash;
// 如果当前正在使用自定义光标,立即更新显示
if (m_bCursorIndex == 254 && m_bIsCtrl && m_bUseCustomCursor) {
if (m_Settings.RemoteCursor) {
// RemoteCursor 模式:触发重绘以显示新光标
Invalidate(FALSE);
} else {
// 窗口类光标模式:更新窗口类光标(避免引用已销毁的光标)
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)m_hCustomCursor);
}
}
}
break;
}
case TOKEN_SCREEN_AUDIO: {
// 音频数据: [token:1][hasFormat:1][AudioFormat?][data...]
if (len > 2) {
OnAudioData(szBuffer + 1, len - 1);
}
break;
}
default: {
Mprintf("CScreenSpyDlg unknown command: %d!!!\n", int(cmd));
}
}
}
VOID CScreenSpyDlg::DrawFirstScreen(void)
{
m_bIsFirst = FALSE;
m_bQualitySwitch = false;
//得到被控端发来的数据 将他拷贝到HBITMAP的缓冲区中这样一个图像就出现了
m_ContextObject->InDeCompressedBuffer.CopyBuffer(m_BitmapData_Full, m_BitmapInfor_Full->bmiHeader.biSizeImage, 1);
PostMessage(WM_PAINT);//触发WM_PAINT消息
}
VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
{
//该函数不是直接画到屏幕上,而是更新一下变化部分的屏幕数据然后调用
//OnPaint画上去
//根据鼠标是否移动和屏幕是否变化判断是否重绘鼠标,防止鼠标闪烁
BOOL bChange = FALSE;
ULONG ulHeadLength = 1 + 1 + sizeof(POINT) + sizeof(BYTE); // 标识 + 算法 + 光标 位置 + 光标类型索引
#if SCREENYSPY_IMPROVE
int frameID = -1;
memcpy(&frameID, m_ContextObject->InDeCompressedBuffer.GetBuffer(ulHeadLength), sizeof(int));
ulHeadLength += sizeof(int);
if (++m_FrameID != frameID) {
TRACE("DrawNextScreenDiff [%d] bmp is lost from %d\n", frameID-m_FrameID, m_FrameID);
m_FrameID = frameID;
}
#else
m_FrameID++;
#endif
LPVOID FirstScreenData = m_BitmapData_Full;
if (FirstScreenData == NULL) return;
LPVOID NextScreenData = m_ContextObject->InDeCompressedBuffer.GetBuffer(ulHeadLength);
ULONG NextScreenLength = m_ContextObject->InDeCompressedBuffer.GetBufferLength() - ulHeadLength;
POINT OldClientCursorPos;
memcpy(&OldClientCursorPos, &m_ClientCursorPos, sizeof(POINT));
memcpy(&m_ClientCursorPos, m_ContextObject->InDeCompressedBuffer.GetBuffer(2), sizeof(POINT));
// 鼠标移动了
if (memcmp(&OldClientCursorPos, &m_ClientCursorPos, sizeof(POINT)) != 0) {
bChange = TRUE;
}
// 光标类型发生变化
BYTE bOldCursorIndex = m_bCursorIndex;
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE;
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
HCURSOR cursor;
if (m_bCursorIndex == 254) { // -2: 使用自定义光标
cursor = (m_bUseCustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
} else if (m_bCursorIndex == 255) { // -1: 不支持,回退到箭头
cursor = LoadCursor(NULL, IDC_ARROW);
} else {
cursor = m_CursorInfo.getCursorHandle(m_bCursorIndex);
if (!cursor) cursor = LoadCursor(NULL, IDC_ARROW);
}
#ifdef _WIN64
SetClassLongPtrA(m_hWnd, GCLP_HCURSOR, (ULONG_PTR)cursor);
#else
SetClassLongA(m_hWnd, GCL_HCURSOR, (LONG)cursor);
#endif
}
}
// 屏幕是否变化
if (NextScreenLength > 0) {
bChange = TRUE;
}
BYTE algorithm = m_ContextObject->InDeCompressedBuffer.GetBYTE(1);
LPBYTE dst = (LPBYTE)FirstScreenData, p = (LPBYTE)NextScreenData;
if (keyFrame) {
switch (algorithm) {
case ALGORITHM_DIFF:
case ALGORITHM_GRAY: {
if (m_BitmapInfor_Full->bmiHeader.biSizeImage == NextScreenLength)
memcpy(dst, p, m_BitmapInfor_Full->bmiHeader.biSizeImage);
break;
}
case ALGORITHM_RGB565: {
// RGB565 关键帧:将 RGB565 整帧还原为 BGRA
ULONG pixelCount = m_BitmapInfor_Full->bmiHeader.biSizeImage / 4;
if (pixelCount * 2 == NextScreenLength)
ConvertRGB565ToBGRA((const uint16_t*)p, dst, pixelCount);
break;
}
case ALGORITHM_H264: {
break;
}
default:
break;
}
} else if (0 != NextScreenLength) {
switch (algorithm) {
case ALGORITHM_DIFF: {
for (LPBYTE end = p + NextScreenLength; p < end; ) {
ULONG ulCount = *(LPDWORD(p + sizeof(ULONG)));
memcpy(dst + *(LPDWORD)p, p + 2 * sizeof(ULONG), ulCount);
p += 2 * sizeof(ULONG) + ulCount;
}
break;
}
case ALGORITHM_GRAY: {
for (LPBYTE end = p + NextScreenLength; p < end; ) {
ULONG ulCount = *(LPDWORD(p + sizeof(ULONG)));
LPBYTE p1 = dst + *(LPDWORD)p, p2 = p + 2 * sizeof(ULONG);
for (int i = 0; i < ulCount; ++i, p1 += 4)
memset(p1, *p2++, sizeof(DWORD));
p += 2 * sizeof(ULONG) + ulCount;
}
break;
}
case ALGORITHM_RGB565: {
// RGB565 差分帧:解码每个差异块
for (LPBYTE end = p + NextScreenLength; p < end; ) {
ULONG pos = *(LPDWORD)p; // BGRA 字节偏移
ULONG pixelCount = *(LPDWORD)(p + sizeof(ULONG)); // 像素数量
p += 2 * sizeof(ULONG);
// RGB565 → BGRA 还原到目标位置
ConvertRGB565ToBGRA((const uint16_t*)p, dst + pos, pixelCount);
p += pixelCount * 2; // RGB565 每像素 2 字节
}
break;
}
case ALGORITHM_H264: {
// Only decode locally if dialog is visible (skip when hidden for web-only viewing)
if (!m_bHide) {
if (Decode((LPBYTE)NextScreenData, NextScreenLength)) {
bChange = TRUE;
}
}
// Broadcast H264 frame to web clients
// Format: [DeviceID:4][FrameType:1][DataLen:4][H264Data:N]
if (NextScreenLength > 0 && WebService().IsRunning()) {
// Detect H264 keyframe by checking NAL unit type
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
bool isKeyFrame = false;
LPBYTE h264Data = (LPBYTE)NextScreenData;
for (ULONG i = 0; i + 4 < NextScreenLength; i++) {
// Look for start code: 0x00 0x00 0x00 0x01 or 0x00 0x00 0x01
if ((h264Data[i] == 0 && h264Data[i+1] == 0 && h264Data[i+2] == 0 && h264Data[i+3] == 1) ||
(h264Data[i] == 0 && h264Data[i+1] == 0 && h264Data[i+2] == 1)) {
int nalOffset = (h264Data[i+2] == 1) ? i + 3 : i + 4;
if (nalOffset < (int)NextScreenLength) {
int nalType = h264Data[nalOffset] & 0x1F;
if (nalType == 5 || nalType == 7 || nalType == 8) {
isKeyFrame = true;
break;
}
}
}
}
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
uint8_t frameType = isKeyFrame ? 1 : 0;
uint32_t dataLen = (uint32_t)NextScreenLength;
memcpy(packet.data(), &deviceIdLow, 4);
packet[4] = frameType;
memcpy(packet.data() + 5, &dataLen, 4);
memcpy(packet.data() + 9, NextScreenData, NextScreenLength);
WebService().BroadcastH264Frame(m_ClientID, packet.data(), packet.size());
}
break;
}
default:
break;
}
}
#if SCREENSPY_WRITE
if (!WriteBitmap(m_BitmapInfor_Full, m_BitmapData_Full, "YAMA", frameID)) {
TRACE("WriteBitmap [%d] failed!!!\n", frameID);
}
#endif
if (bChange && !m_bHide) {
PostMessage(WM_PAINT);
}
}
VOID CScreenSpyDlg::DrawScrollFrame()
{
// 滚动帧消息格式:
// 字节 0: TOKEN_SCROLL_FRAME (99)
// 字节 1: 算法类型
// 字节 2-5: 光标 X
// 字节 6-9: 光标 Y
// 字节 10: 光标类型
// 字节 11: 滚动方向 (0=上, 1=下)
// 字节 12-15: 滚动距离 (像素)
// 字节 16-19: 边缘数据偏移 (字节)
// 字节 20-23: 边缘数据长度 (像素数)
// 字节 24+: 边缘像素数据
BOOL bChange = FALSE;
ULONG ulHeadLength = 1 + 1 + sizeof(POINT) + sizeof(BYTE); // 11 字节
LPVOID FirstScreenData = m_BitmapData_Full;
if (FirstScreenData == NULL) return;
// 读取光标位置
POINT OldClientCursorPos;
memcpy(&OldClientCursorPos, &m_ClientCursorPos, sizeof(POINT));
memcpy(&m_ClientCursorPos, m_ContextObject->InDeCompressedBuffer.GetBuffer(2), sizeof(POINT));
if (memcmp(&OldClientCursorPos, &m_ClientCursorPos, sizeof(POINT)) != 0) {
bChange = TRUE;
}
// 读取光标类型
BYTE bOldCursorIndex = m_bCursorIndex;
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE;
}
// 读取滚动参数
LPBYTE buffer = m_ContextObject->InDeCompressedBuffer.GetBuffer(ulHeadLength);
BYTE direction = buffer[0];
int scrollAmount = *(int*)(buffer + 1);
int edgeOffset = *(int*)(buffer + 5);
int edgePixelCount = *(int*)(buffer + 9);
LPBYTE edgeData = buffer + 13;
BYTE algorithm = m_ContextObject->InDeCompressedBuffer.GetBYTE(1);
LPBYTE dst = (LPBYTE)FirstScreenData;
// 1. 滚动现有位图缓冲区
// BMP is bottom-up: row 0 = screen bottom, row height-1 = screen top
int stride = m_BitmapInfor_Full->bmiHeader.biWidth * 4;
int height = m_BitmapInfor_Full->bmiHeader.biHeight;
int copyHeight = height - scrollAmount;
if (direction == SCROLL_DIR_UP) {
// 向上滚动屏幕内容向下移BMP 中高地址移到低地址
// 新内容填充到 BMP 高地址(屏幕顶部)
memmove(dst, dst + scrollAmount * stride, copyHeight * stride);
} else {
// 向下滚动屏幕内容向上移BMP 中低地址移到高地址
// 新内容填充到 BMP 低地址(屏幕底部)
memmove(dst + scrollAmount * stride, dst, copyHeight * stride);
}
// 2. 复制边缘数据
LPBYTE edgeDst = dst + edgeOffset;
switch (algorithm) {
case ALGORITHM_RGB565:
ConvertRGB565ToBGRA((const uint16_t*)edgeData, edgeDst, edgePixelCount);
break;
case ALGORITHM_GRAY:
for (int i = 0; i < edgePixelCount; ++i, edgeDst += 4) {
memset(edgeDst, edgeData[i], 3);
edgeDst[3] = 0xFF;
}
break;
case ALGORITHM_DIFF:
default:
memcpy(edgeDst, edgeData, edgePixelCount * 4);
break;
}
bChange = TRUE;
m_FrameID++;
if (bChange && !m_bHide) {
PostMessage(WM_PAINT);
}
}
bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size)
{
// 分辨率切换中,跳过解码避免访问无效缓冲区
// 但加入超时保护5秒内客户端未响应则恢复防止卡死
if (m_bResolutionChanging) {
if (GetTickCount64() - m_AdaptiveQuality.lastResChangeTime > 5000) {
m_bResolutionChanging = false; // 超时恢复
// 刷新解码器状态,丢弃之前的参考帧,等待下一个 I 帧
if (m_pCodecContext) {
avcodec_flush_buffers(m_pCodecContext);
}
Mprintf("分辨率切换超时,刷新解码器并恢复\n");
} else {
return false;
}
}
if (!m_BitmapData_Full || !m_BitmapInfor_Full) {
return false;
}
// 解码数据.
av_init_packet(&m_AVPacket);
m_AVPacket.data = (uint8_t*)Buffer;
m_AVPacket.size = size;
int err = avcodec_send_packet(m_pCodecContext, &m_AVPacket);
if (!err) {
err = avcodec_receive_frame(m_pCodecContext, &m_AVFrame);
if (err == AVERROR(EAGAIN)) {
Mprintf("avcodec_receive_frame error: EAGAIN\n");
return false;
}
// 解码数据前会清除m_AVFrame的内容.
if (!err) {
LPVOID Image[2] = { 0 };
LPVOID CursorInfo[2] = { 0 };
//成功.
//I420 ---> ARGB.
//WaitForSingleObject(m_hMutex,INFINITE);
libyuv::I420ToARGB(
m_AVFrame.data[0], m_AVFrame.linesize[0],
m_AVFrame.data[1], m_AVFrame.linesize[1],
m_AVFrame.data[2], m_AVFrame.linesize[2],
(uint8_t*)m_BitmapData_Full,
m_BitmapInfor_Full->bmiHeader.biWidth*4,
m_BitmapInfor_Full->bmiHeader.biWidth,
m_BitmapInfor_Full->bmiHeader.biHeight);
return true;
}
Mprintf("avcodec_receive_frame failed with error: %d\n", err);
} else {
Mprintf("avcodec_send_packet failed with error: %d\n", err);
}
return false;
}
void CScreenSpyDlg::OnPaint()
{
if (m_bIsClosed) return;
CPaintDC dc(this); // device context for painting
if (m_bIsFirst) {
if (!m_bQualitySwitch)
DrawTipString(_TR("请等待......"));
return;
}
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
if (m_bAdaptiveSize) {
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
// 尺寸相同时用 BitBlt更快否则用 StretchBlt
if (srcW == dstW && srcH == dstH) {
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, 0, 0, SRCCOPY);
} else {
StretchBlt(m_hFullDC, 0, 0, dstW, dstH, m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY);
}
} else {
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
}
if ((m_bIsCtrl && m_Settings.RemoteCursor) || m_bIsTraceCursor) {
CPoint ptLocal;
GetCursorPos(&ptLocal);
ScreenToClient(&ptLocal);
CRect rcToolbar(0, 0, 0, 0);
if (m_pToolbar) m_pToolbar->GetWindowRect(&rcToolbar), ScreenToClient(&rcToolbar);
// 只有当本地鼠标不在工具栏区域时,才绘制远程位图光标
if (!rcToolbar.PtInRect(ptLocal)) {
// 1. 计算缩放位置
int drawX = m_bAdaptiveSize ? (int)(m_ClientCursorPos.x / m_wZoom) : (m_ClientCursorPos.x - m_ulHScrollPos);
int drawY = m_bAdaptiveSize ? (int)(m_ClientCursorPos.y / m_hZoom) : (m_ClientCursorPos.y - m_ulVScrollPos);
// 2. 获取光标句柄(支持自定义光标)
HCURSOR hCursor;
if (m_bCursorIndex == 254) { // -2: 使用自定义光标
hCursor = (m_bUseCustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
} else if (m_bCursorIndex == 255) { // -1: 不支持,回退到箭头
hCursor = LoadCursor(NULL, IDC_ARROW);
} else {
hCursor = m_CursorInfo.getCursorHandle(m_bCursorIndex);
if (!hCursor) hCursor = LoadCursor(NULL, IDC_ARROW);
}
// 3. 强制绘制
DrawIconEx(
m_hFullDC,
drawX,
drawY,
hCursor,
0, 0, 0, NULL, DI_NORMAL | DI_COMPAT
);
}
}
if (!m_bConnected && GetTickCount64() - m_nDisconnectTime>2000) {
DrawTipString(_TR("正在重连......"), 2);
}
// 截图保存提示 (显示3秒断线重连时不显示避免重叠)
if (m_bConnected && !m_strSaveNotice.empty() && GetTickCount64() - m_nSaveNoticeTime < 3000) {
RECT rcClient;
GetClientRect(&rcClient);
int cw = rcClient.right - rcClient.left;
int ch = rcClient.bottom - rcClient.top;
CString tipText;
tipText.FormatL("已保存: %s", m_strSaveNotice.c_str());
// 计算文字尺寸
HFONT hFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
HFONT hOldFont = (HFONT)SelectObject(m_hFullDC, hFont);
SIZE textSize;
GetTextExtentPoint32(m_hFullDC, tipText, tipText.GetLength(), &textSize);
int padding = 10;
int bannerW = textSize.cx + padding * 2;
int bannerH = textSize.cy + padding * 2;
int bannerX = (cw - bannerW) / 2;
int bannerY = ch - bannerH - 40;
// 半透明黑色背景
HDC hMemDC = CreateCompatibleDC(m_hFullDC);
HBITMAP hBmp = CreateCompatibleBitmap(m_hFullDC, bannerW, bannerH);
HBITMAP hOldBmp = (HBITMAP)SelectObject(hMemDC, hBmp);
HBRUSH hBrush = CreateSolidBrush(RGB(0, 0, 0));
RECT rcBanner = { 0, 0, bannerW, bannerH };
FillRect(hMemDC, &rcBanner, hBrush);
DeleteObject(hBrush);
BLENDFUNCTION blend = { 0 };
blend.BlendOp = AC_SRC_OVER;
blend.SourceConstantAlpha = 180;
AlphaBlend(m_hFullDC, bannerX, bannerY, bannerW, bannerH,
hMemDC, 0, 0, bannerW, bannerH, blend);
SelectObject(hMemDC, hOldBmp);
DeleteObject(hBmp);
DeleteDC(hMemDC);
// 绘制白色文字
int oldBkMode = SetBkMode(m_hFullDC, TRANSPARENT);
COLORREF oldTextClr = SetTextColor(m_hFullDC, RGB(255, 255, 255));
RECT rcText = { bannerX + padding, bannerY + padding,
bannerX + bannerW - padding, bannerY + bannerH - padding };
DrawText(m_hFullDC, tipText, -1, &rcText, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
SetBkMode(m_hFullDC, oldBkMode);
SetTextColor(m_hFullDC, oldTextClr);
SelectObject(m_hFullDC, hOldFont);
} else if (!m_strSaveNotice.empty()) {
m_strSaveNotice.clear();
}
}
BOOL CScreenSpyDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
if ((m_bIsCtrl && m_Settings.RemoteCursor) && nHitTest == HTCLIENT) {
::SetCursor(NULL); // 只要在客户区,始终隐藏系统光标
return TRUE; // 告诉 Windows 我们处理过了
}
return __super::OnSetCursor(pWnd, nHitTest, message);
}
VOID CScreenSpyDlg::DrawTipString(CString strString, int fillMode)
{
// fillMode: 0=不填充, 1=全黑, 2=半透明
RECT Rect;
GetClientRect(&Rect);
int width = Rect.right - Rect.left;
int height = Rect.bottom - Rect.top;
if (fillMode == 1) {
// 原来的全黑效果
COLORREF BackgroundColor = RGB(0x00, 0x00, 0x00);
SetBkColor(m_hFullDC, BackgroundColor);
ExtTextOut(m_hFullDC, 0, 0, ETO_OPAQUE, &Rect, NULL, 0, NULL);
} else if (fillMode == 2) {
// 半透明效果
HDC hMemDC = CreateCompatibleDC(m_hFullDC);
HBITMAP hBitmap = CreateCompatibleBitmap(m_hFullDC, width, height);
HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemDC, hBitmap);
HBRUSH hBrush = CreateSolidBrush(RGB(0, 0, 0));
FillRect(hMemDC, &Rect, hBrush);
DeleteObject(hBrush);
BLENDFUNCTION blend = { 0 };
blend.BlendOp = AC_SRC_OVER;
blend.SourceConstantAlpha = 150;
blend.AlphaFormat = 0;
AlphaBlend(m_hFullDC, 0, 0, width, height,
hMemDC, 0, 0, width, height, blend);
SelectObject(hMemDC, hOldBitmap);
DeleteObject(hBitmap);
DeleteDC(hMemDC);
}
SetBkMode(m_hFullDC, TRANSPARENT);
SetTextColor(m_hFullDC, RGB(0xff, 0x00, 0x00));
DrawText(m_hFullDC, strString, -1, &Rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
}
bool DirectoryExists(const char* path)
{
DWORD attr = GetFileAttributesA(path);
return (attr != INVALID_FILE_ATTRIBUTES &&
(attr & FILE_ATTRIBUTE_DIRECTORY));
}
std::string GetScreenShotPath(CWnd *parent, const CString& ip, const CString &filter, const CString& suffix)
{
std::string path;
std::string folder = THIS_CFG.GetStr("settings", "ScreenShot", "");
if (folder.empty() || !DirectoryExists(folder.c_str())) {
CString strFileName = ip + CTime::GetCurrentTime().FormatL(_T("_%Y%m%d%H%M%S.")) + suffix;
CFileDialog dlg(FALSE, suffix, strFileName, OFN_OVERWRITEPROMPT, filter, parent);
if (dlg.DoModal() != IDOK)
return "";
folder = dlg.GetFolderPath();
if (!folder.empty() && folder.back() != '\\') {
folder += '\\';
}
path = dlg.GetPathName();
THIS_CFG.SetStr("settings", "ScreenShot", folder);
} else {
if (!folder.empty() && folder.back() != '\\') {
folder += '\\';
}
path = folder + std::string(ip) + "_" + ToPekingDateTime(0) + "." + std::string(suffix);
}
return path;
}
void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CMenu* SysMenu = GetSystemMenu(FALSE);
switch (nID) {
case IDM_CONTROL: {
m_bIsCtrl = !m_bIsCtrl;
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
// 控制模式:禁用本地 IME查看模式启用本地 IME
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
break;
}
case IDM_FULLSCREEN: { // 全屏
EnterFullScreen();
SysMenu->CheckMenuItem(IDM_FULLSCREEN, MF_CHECKED); //菜单样式
BYTE cmd[4] = { CMD_FULL_SCREEN, m_Settings.FullScreen = TRUE };
m_ContextObject->Send2Client(cmd, sizeof(cmd));
break;
}
case IDM_REMOTE_CURSOR: {
BYTE cmd[4] = { CMD_REMOTE_CURSOR, m_Settings.RemoteCursor = !m_Settings.RemoteCursor };
SysMenu->CheckMenuItem(IDM_REMOTE_CURSOR, m_Settings.RemoteCursor ? MF_CHECKED : MF_UNCHECKED);
m_ContextObject->Send2Client(cmd, sizeof(cmd));
break;
}
case IDM_SAVEDIB: { // 快照保存
SaveSnapshot();
break;
}
case IDM_SAVEAVI:
case IDM_SAVEAVI_H264: {
if (SysMenu->GetMenuState(nID, MF_BYCOMMAND) & MF_CHECKED) {
KillTimer(TIMER_ID);
SysMenu->CheckMenuItem(nID, MF_UNCHECKED);
SysMenu->EnableMenuItem(IDM_SAVEAVI, MF_ENABLED);
SysMenu->EnableMenuItem(IDM_SAVEAVI_H264, MF_ENABLED);
m_aviFile = "";
m_aviStream.Close();
return;
}
m_aviFile = GetScreenShotPath(this, m_IPAddress, "Video(*.avi)|*.avi|", "avi").c_str();
const int duration = 250, rate = 1000 / duration;
FCCHandler handler = nID == IDM_SAVEAVI ? ENCODER_MJPEG : ENCODER_H264;
int code;
if (code = m_aviStream.Open(m_aviFile, m_BitmapInfor_Full, rate, handler)) {
MessageBoxL(CString("Create Video(*.avi) Failed:\n") + m_aviFile + "\r\n" + _TR("错误代码: ") +
CBmpToAvi::GetErrMsg(code).c_str(), "提示", MB_ICONINFORMATION);
m_aviFile = _T("");
} else {
::SetTimer(m_hWnd, TIMER_ID, duration, NULL);
SysMenu->CheckMenuItem(nID, MF_CHECKED);
SysMenu->EnableMenuItem(nID == IDM_SAVEAVI ? IDM_SAVEAVI_H264 : IDM_SAVEAVI, MF_DISABLED);
}
break;
}
case IDM_SWITCHSCREEN: {
BYTE bToken[2] = { COMMAND_SWITCH_SCREEN };
m_ContextObject->Send2Client(bToken, sizeof(bToken));
break;
}
case IDM_MULTITHREAD_COMPRESS: {
int threadNum = m_Settings.CompressThread;
threadNum = 4 - threadNum;
BYTE bToken[2] = { CMD_MULTITHREAD_COMPRESS, (BYTE)threadNum };
m_ContextObject->Send2Client(bToken, sizeof(bToken));
SysMenu->CheckMenuItem(nID, threadNum ? MF_CHECKED : MF_UNCHECKED);
m_Settings.CompressThread = threadNum;
break;
}
case IDM_ORIGINAL_SIZE: {
const int strategy = 1;
BYTE cmd[16] = { CMD_SCREEN_SIZE, (BYTE)strategy };
m_ContextObject->Send2Client(cmd, sizeof(cmd));
m_Settings.ScreenStrategy = strategy;
SysMenu->CheckMenuItem(IDM_ORIGINAL_SIZE, MF_CHECKED);
SysMenu->CheckMenuItem(IDM_SCREEN_1080P, MF_UNCHECKED);
break;
}
case IDM_SCREEN_1080P: {
const int strategy = 0;
BYTE cmd[16] = { CMD_SCREEN_SIZE, (BYTE)strategy };
m_ContextObject->Send2Client(cmd, sizeof(cmd));
m_Settings.ScreenStrategy = strategy;
SysMenu->CheckMenuItem(IDM_SCREEN_1080P, MF_CHECKED);
SysMenu->CheckMenuItem(IDM_ORIGINAL_SIZE, MF_UNCHECKED);
break;
}
case IDM_ENABLE_SSE2: {
BYTE cmd[4] = { CMD_INSTRUCTION_SET, m_Settings.CpuSpeedup = !m_Settings.CpuSpeedup };
m_ContextObject->Send2Client(cmd, sizeof(cmd));
SysMenu->CheckMenuItem(IDM_ENABLE_SSE2, m_Settings.CpuSpeedup == 1 ? MF_CHECKED : MF_UNCHECKED);
break;
}
case IDM_FAST_STRETCH: {
bool bFast = !GetFastStretchMode();
SetFastStretchMode(bFast);
SysMenu->CheckMenuItem(IDM_FAST_STRETCH, bFast ? MF_CHECKED : MF_UNCHECKED);
// 更新当前窗口的 StretchBltMode
SetStretchBltMode(m_hFullDC, bFast ? COLORONCOLOR : HALFTONE);
if (!bFast) SetBrushOrgEx(m_hFullDC, 0, 0, NULL);
break;
}
case IDM_CUSTOM_CURSOR: {
m_bUseCustomCursor = !m_bUseCustomCursor;
SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_bUseCustomCursor ? MF_CHECKED : MF_UNCHECKED);
// 如果当前是自定义光标,立即更新显示
if (m_bCursorIndex == 254 && m_bIsCtrl) {
HCURSOR cursor = (m_bUseCustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
if (m_Settings.RemoteCursor) {
Invalidate(FALSE);
} else {
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)cursor);
}
}
break;
}
case IDM_FPS_10:
case IDM_FPS_15:
case IDM_FPS_20:
case IDM_FPS_25:
case IDM_FPS_30:
case IDM_FPS_UNLIMITED: {
int fps = 10 + (nID - IDM_FPS_10) * 5;
BYTE bToken[2] = { CMD_FPS, nID == IDM_FPS_UNLIMITED ? 255 : fps };
m_ContextObject->Send2Client(bToken, sizeof(bToken));
m_Settings.MaxFPS = nID == IDM_FPS_UNLIMITED ? 255 : fps;
for (int i = IDM_FPS_10; i <= IDM_FPS_UNLIMITED; i++) {
SysMenu->CheckMenuItem(i, MF_UNCHECKED);
}
SysMenu->CheckMenuItem(nID, MF_CHECKED);
break;
}
case IDM_SCROLL_DETECT_OFF:
case IDM_SCROLL_DETECT_2:
case IDM_SCROLL_DETECT_4:
case IDM_SCROLL_DETECT_8: {
// 菜单ID到间隔值的映射
int interval = nID == IDM_SCROLL_DETECT_OFF ? 0 :
nID == IDM_SCROLL_DETECT_2 ? 2 :
nID == IDM_SCROLL_DETECT_4 ? 4 : 8;
m_Settings.ScrollDetectInterval = interval;
// 更新菜单选中状态
SysMenu->CheckMenuItem(IDM_SCROLL_DETECT_OFF, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_SCROLL_DETECT_2, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_SCROLL_DETECT_4, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_SCROLL_DETECT_8, MF_UNCHECKED);
SysMenu->CheckMenuItem(nID, MF_CHECKED);
// 实时通知客户端更新间隔
BYTE cmd[8] = { CMD_SCROLL_INTERVAL };
memcpy(cmd + 1, &interval, sizeof(int));
m_ContextObject->Send2Client(cmd, 1 + sizeof(int));
break;
}
case IDM_QUALITY_OFF: { // 关闭质量控制
m_AdaptiveQuality.enabled = false;
m_Settings.QualityLevel = QUALITY_DISABLED;
UpdateQualityMenuCheck(SysMenu);
// 发送给客户端保存 (QualityLevel=-2 表示关闭)
BYTE cmd[4] = { CMD_QUALITY_LEVEL, (BYTE)QUALITY_DISABLED, 1 }; // persist=1
m_ContextObject->Send2Client(cmd, sizeof(cmd));
UpdateWindowTitle();
Mprintf("关闭质量控制,使用原有算法设置\n");
break;
}
case IDM_ADAPTIVE_QUALITY: { // 自适应质量开关
m_AdaptiveQuality.enabled = true;
m_Settings.QualityLevel = QUALITY_ADAPTIVE;
UpdateQualityMenuCheck(SysMenu);
// 发送给客户端保存 (QualityLevel=-1 表示自适应)
BYTE cmd[4] = { CMD_QUALITY_LEVEL, (BYTE)QUALITY_ADAPTIVE, 1 }; // persist=1
m_ContextObject->Send2Client(cmd, sizeof(cmd));
// 启用时立即评估一次
EvaluateQuality();
UpdateWindowTitle();
break;
}
case IDM_QUALITY_ULTRA:
case IDM_QUALITY_HIGH:
case IDM_QUALITY_GOOD:
case IDM_QUALITY_MEDIUM:
case IDM_QUALITY_LOW:
case IDM_QUALITY_MINIMAL: {
// 手动设置质量等级
int level = nID - IDM_QUALITY_ULTRA;
// 关闭自适应模式
m_AdaptiveQuality.enabled = false;
m_Settings.QualityLevel = level;
// 应用选择的等级 (persist=true 保存到客户端)
ApplyQualityLevel(level, true);
UpdateQualityMenuCheck(SysMenu);
UpdateWindowTitle();
Mprintf("手动设置质量: %s\n", GetQualityName(level));
break;
}
case IDM_TRACE_CURSOR: { // 跟踪被控端鼠标
m_bIsTraceCursor = !m_bIsTraceCursor;
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
// 不再强制切换自适应模式,两种模式都支持跟踪光标
// 重绘消除或显示鼠标
Invalidate(FALSE);
break;
}
case IDM_BLOCK_INPUT: { // 锁定服务端鼠标和键盘
BOOL bIsChecked = SysMenu->GetMenuState(IDM_BLOCK_INPUT, MF_BYCOMMAND) & MF_CHECKED;
SysMenu->CheckMenuItem(IDM_BLOCK_INPUT, bIsChecked ? MF_UNCHECKED : MF_CHECKED);
BYTE bToken[2];
bToken[0] = COMMAND_SCREEN_BLOCK_INPUT;
bToken[1] = !bIsChecked;
m_ContextObject->Send2Client(bToken, sizeof(bToken));
// 同步工具栏按钮状态
if (m_pToolbar) {
m_pToolbar->m_bBlockInput = !bIsChecked;
m_pToolbar->UpdateButtonIcons();
}
break;
}
case IDM_RESTORE_CONSOLE: { // RDP会话归位
BYTE bToken = CMD_RESTORE_CONSOLE;
m_ContextObject->Send2Client(&bToken, 1);
break;
}
case IDM_RESET_VIRTUAL_DESKTOP: { // 重置虚拟桌面
BYTE bToken = CMD_RESET_VIRTUAL_DESKTOP;
m_ContextObject->Send2Client(&bToken, 1);
break;
}
case IDM_GET_CLIPBOARD: { //想要Client的剪贴板内容
BYTE bToken = COMMAND_SCREEN_GET_CLIPBOARD;
m_ContextObject->Send2Client(&bToken, sizeof(bToken));
break;
}
case IDM_SET_CLIPBOARD: { //给他
SendServerClipboard();
break;
}
case IDM_ADAPTIVE_SIZE: {
m_bAdaptiveSize = !m_bAdaptiveSize;
if (!m_bAdaptiveSize && m_BitmapInfor_Full) {
SetScrollRange(SB_HORZ, 0, m_BitmapInfor_Full->bmiHeader.biWidth);
SetScrollRange(SB_VERT, 0, m_BitmapInfor_Full->bmiHeader.biHeight);
}
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
break;
}
case IDM_AUDIO_TOGGLE: {
m_Settings.AudioEnabled = !m_Settings.AudioEnabled;
SendAudioCtrl(m_Settings.AudioEnabled ? CYCLEAUDIO_ENABLE : CYCLEAUDIO_DISABLE, 1);
SysMenu->CheckMenuItem(IDM_AUDIO_TOGGLE, m_Settings.AudioEnabled ? MF_CHECKED : MF_UNCHECKED);
if (!m_Settings.AudioEnabled) {
StopAudioPlayback();
}
break;
}
}
__super::OnSysCommand(nID, lParam);
}
void CScreenSpyDlg::OnTimer(UINT_PTR nIDEvent)
{
if (!m_aviFile.IsEmpty()) {
LPCTSTR lpTipsString = _T("");
m_aviStream.Write((BYTE*)m_BitmapData_Full);
// 提示正在录像
SetTextColor(m_hFullDC, RGB(0xff, 0x00, 0x00));
TextOut(m_hFullDC, 0, 0, lpTipsString, lstrlen(lpTipsString));
}
if (nIDEvent == 1 && m_Settings.FullScreen && m_pToolbar) {
m_pToolbar->CheckMousePosition();
}
if (nIDEvent == 2) {
KillTimer(2);
if (m_bConnected) {
return;
}
CWnd* pMain = AfxGetMainWnd();
if (pMain)
::PostMessageA(pMain->GetSafeHwnd(), WM_SHOWNOTIFY, (WPARAM)new CharMsg(_TR("连接已断开")),
(LPARAM)new CharMsg(m_IPAddress + _TR(" - 远程桌面连接已断开")));
this->PostMessageA(WM_CLOSE, 0, 0);
return;
}
if (nIDEvent == 3) {
KillTimer(3);
PostMessageA(WM_PAINT);
}
if (nIDEvent == 4) {
// 计算传输速率并更新标题
m_dTransferRate = m_ulBytesThisSecond / 1024.0; // KB/s
m_ulBytesThisSecond = 0;
// 使用EMA平滑帧率 (alpha=0.3),避免跳跃
double currentFPS = (double)m_ulFramesThisSecond;
if (m_dFrameRate == 0) {
m_dFrameRate = currentFPS; // 首次直接赋值
} else {
m_dFrameRate = 0.3 * currentFPS + 0.7 * m_dFrameRate;
}
m_ulFramesThisSecond = 0;
// 自适应质量评估
EvaluateQuality();
UpdateWindowTitle();
// 更新全屏状态信息窗口
if (m_pStatusInfoWnd && m_pStatusInfoWnd->IsVisible()) {
CString qualityName = m_Settings.QualityLevel == QUALITY_DISABLED ? _T("Off") :
CString(GetQualityName(m_AdaptiveQuality.currentLevel));
m_pStatusInfoWnd->UpdateInfo(m_dFrameRate, m_dTransferRate, qualityName);
}
}
__super::OnTimer(nIDEvent);
}
void CScreenSpyDlg::UpdateWindowTitle()
{
if (!m_BitmapInfor_Full) return;
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = m_BitmapInfor_Full->bmiHeader.biHeight;
// 构建质量名称:关闭显示 "Off",自适应和手动都会显示 "当前等级"
CString qualityName = m_Settings.QualityLevel == QUALITY_DISABLED ? "" :
CString(" | ") + GetQualityName(m_AdaptiveQuality.currentLevel);
CString strTitle;
UINT fps = (UINT)(m_dFrameRate + 0.5); // 四舍五入显示
if (m_dTransferRate >= 1024) {
strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.1f MB/s%s",
m_IPAddress, width, height, fps, m_dTransferRate / 1024, qualityName.GetString());
} else {
strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.0f KB/s%s",
m_IPAddress, width, height, fps, m_dTransferRate, qualityName.GetString());
}
// 追加剪贴板操作提示
strTitle += (m_Settings.ScreenType == 2) ?
_TR(" [远程右键复制, 本地 Ctrl+V 粘贴]") : _TR(" [远程 Ctrl+C 复制, 本地 Ctrl+V 粘贴]");
SetWindowText(strTitle);
}
const char* CScreenSpyDlg::GetQualityName(int level)
{
static const char* names[] = {
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
};
if (level == QUALITY_DISABLED)
return "Off";
if (level == QUALITY_ADAPTIVE)
return "Auto";
if (level >= 0 && level < QUALITY_COUNT)
return names[level];
return "Unknown";
}
void CScreenSpyDlg::UpdateQualityMenuCheck(CMenu* SysMenu)
{
if (!SysMenu) SysMenu = GetSystemMenu(FALSE);
// 先全部取消勾选
SysMenu->CheckMenuItem(IDM_QUALITY_OFF, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_ADAPTIVE_QUALITY, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_QUALITY_ULTRA, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_QUALITY_HIGH, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_QUALITY_GOOD, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_QUALITY_MEDIUM, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_QUALITY_LOW, MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_QUALITY_MINIMAL, MF_UNCHECKED);
// 勾选当前项
if (m_Settings.QualityLevel == QUALITY_DISABLED) {
SysMenu->CheckMenuItem(IDM_QUALITY_OFF, MF_CHECKED);
} else if (m_AdaptiveQuality.enabled) {
SysMenu->CheckMenuItem(IDM_ADAPTIVE_QUALITY, MF_CHECKED);
} else if (m_AdaptiveQuality.currentLevel >= 0 && m_AdaptiveQuality.currentLevel < QUALITY_COUNT) {
SysMenu->CheckMenuItem(IDM_QUALITY_ULTRA + m_AdaptiveQuality.currentLevel, MF_CHECKED);
}
}
int CScreenSpyDlg::GetClientRTT()
{
if (!m_pParent || !m_ClientID) return 0;
// 遍历列表查找对应的 ClientID
auto ctx = m_pParent->FindHost(m_ClientID);
if (ctx) {
auto pingStr = ctx->GetClientData(ONLINELIST_PING);
return _ttoi(pingStr);
}
return 0;
}
void CScreenSpyDlg::EvaluateQuality()
{
if (!m_AdaptiveQuality.enabled) return;
// 0. 启动延迟1分钟内不进行自适应调整
ULONGLONG now = GetTickCount64();
if (now - m_AdaptiveQuality.startTime < 60000) return;
// 1. 获取RTT
int rtt = GetClientRTT();
if (rtt <= 0) return;
m_AdaptiveQuality.lastRTT = rtt;
// 2. 计算目标等级
int targetLevel = GetTargetQualityLevel(rtt, m_bUsingFRP);
int currentLevel = m_AdaptiveQuality.currentLevel;
if (targetLevel == currentLevel) {
m_AdaptiveQuality.stableCount = 0;
return;
}
// 3. 检查是否涉及分辨率变化
int currentMaxWidth = GetQualityProfile(currentLevel).maxWidth;
int targetMaxWidth = GetQualityProfile(targetLevel).maxWidth;
bool resolutionChange = (currentMaxWidth != targetMaxWidth);
// 4. 冷却时间检查
// - 分辨率变化降级15秒升级30秒
// - 纯FPS/算法变化3秒
ULONGLONG cooldown = 3000;
if (resolutionChange) {
cooldown = (targetLevel > currentLevel) ? 15000 : 30000;
if (now - m_AdaptiveQuality.lastResChangeTime < cooldown)
return;
} else {
if (now - m_AdaptiveQuality.lastChangeTime < cooldown)
return;
}
// 5. 降级:快速响应 (连续2次)
if (targetLevel > currentLevel) {
m_AdaptiveQuality.stableCount++;
if (m_AdaptiveQuality.stableCount >= 2) {
ApplyQualityLevel(targetLevel);
}
}
// 6. 升级:谨慎处理 (连续5次且只升一级)
else if (targetLevel < currentLevel) {
m_AdaptiveQuality.stableCount++;
if (m_AdaptiveQuality.stableCount >= 5) {
ApplyQualityLevel(currentLevel - 1);
}
}
}
void CScreenSpyDlg::ApplyQualityLevel(int level, bool persist)
{
if (level < 0 || level >= QUALITY_COUNT) return;
const QualityProfile& profile = GetQualityProfile(level);
int oldMaxWidth = m_AdaptiveQuality.currentMaxWidth;
int newMaxWidth = profile.maxWidth;
Mprintf("ApplyQualityLevel: level=%d, oldMaxWidth=%d, newMaxWidth=%d, algo=%d\n",
level, oldMaxWidth, newMaxWidth, profile.algorithm);
m_AdaptiveQuality.currentLevel = level;
m_AdaptiveQuality.lastChangeTime = GetTickCount64();
m_AdaptiveQuality.stableCount = 0;
// 1. 发送 FPS 和算法 (persist=1 表示手动选择需要保存)
BYTE cmd[4] = { CMD_QUALITY_LEVEL, (BYTE)level, (BYTE)persist };
m_ContextObject->Send2Client(cmd, sizeof(cmd));
// 2. 如果分辨率变化,发送 CMD_SCREEN_SIZE
if (newMaxWidth != oldMaxWidth) {
// 标记分辨率切换中,阻止 Decode 访问旧缓冲区
m_bResolutionChanging = true;
// 自适应调整且分辨率变化时不显示"请等待",手动切换时显示
if (!persist) m_bQualitySwitch = true;
m_AdaptiveQuality.currentMaxWidth = newMaxWidth;
m_AdaptiveQuality.lastResChangeTime = GetTickCount64();
BYTE sizeCmd[16] = { CMD_SCREEN_SIZE, 2 }; // strategy=2 表示自定义maxWidth
memcpy(sizeCmd + 2, &newMaxWidth, sizeof(int));
m_ContextObject->Send2Client(sizeCmd, 10);
Mprintf("发送 CMD_SCREEN_SIZE: strategy=2, maxWidth=%d\n", newMaxWidth);
} else {
Mprintf("分辨率未变化,不发送 CMD_SCREEN_SIZE\n");
}
Mprintf("质量切换: %s (FPS=%d, Algo=%d)\n", GetQualityName(level), profile.maxFPS, profile.algorithm);
}
BOOL CScreenSpyDlg::PreTranslateMessage(MSG* pMsg)
{
#define MAKEDWORD(h,l) (((unsigned long)h << 16) | l)
switch (pMsg->message) {
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_LBUTTONDBLCLK:
case WM_RBUTTONDBLCLK:
case WM_MBUTTONDBLCLK:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_MOUSEMOVE:
SendScaledMouseMessage(pMsg, true);
break;
case WM_MOUSEWHEEL: {
// WM_MOUSEWHEEL 的 lParam 是屏幕坐标,需要转换为客户区坐标
POINT pt = { GET_X_LPARAM(pMsg->lParam), GET_Y_LPARAM(pMsg->lParam) };
ScreenToClient(&pt);
MSG wheelMsg = *pMsg;
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
SendScaledMouseMessage(&wheelMsg, true);
}
break;
case WM_KEYDOWN:
case WM_KEYUP:
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
if (pMsg->message == WM_KEYDOWN && m_Settings.FullScreen) {
// Ctrl+Alt+Home 退出全屏(备用)
if (pMsg->wParam == VK_HOME &&
(GetKeyState(VK_CONTROL) & 0x8000) &&
(GetKeyState(VK_MENU) & 0x8000)) {
LeaveFullScreen();
BYTE cmd[4] = { CMD_FULL_SCREEN, m_Settings.FullScreen = FALSE };
m_ContextObject->Send2Client(cmd, sizeof(cmd));
return TRUE;
}
}
if (pMsg->wParam != VK_LWIN && pMsg->wParam != VK_RWIN) {
SendScaledMouseMessage(pMsg, false); // false: 保留键盘 lParam
}
if (pMsg->message == WM_SYSKEYDOWN && pMsg->wParam == VK_F4) {
return TRUE; // 屏蔽 Alt + F4
}
if (pMsg->wParam == VK_RETURN || pMsg->wParam == VK_ESCAPE)
return TRUE;// 屏蔽Enter和ESC关闭对话
break;
}
return __super::PreTranslateMessage(pMsg);
}
void CScreenSpyDlg::SendScaledMouseMessage(MSG* pMsg, bool makeLP)
{
if (!m_bIsCtrl || !m_bConnected)
return;
MYMSG msg(*pMsg);
LONG x = LOWORD(pMsg->lParam), y = HIWORD(pMsg->lParam);
LONG low = m_bAdaptiveSize ? x * m_wZoom : x + m_ulHScrollPos;
LONG high = m_bAdaptiveSize ? y * m_hZoom : y + m_ulVScrollPos;
if (makeLP) msg.lParam = MAKELPARAM(low, high);
msg.pt.x = low;
msg.pt.y = high;
SendCommand(&msg);
}
VOID CScreenSpyDlg::SendCommand(const MYMSG* Msg)
{
if (!m_bIsCtrl)
return;
if (Msg->message == WM_MOUSEMOVE) {
auto now = clock();
auto time_elapsed = now - m_lastMouseMove;
int dx = abs(Msg->pt.x - m_lastMousePoint.x);
int dy = abs(Msg->pt.y - m_lastMousePoint.y);
int dist_sq = dx * dx + dy * dy;
bool fastMove = dist_sq > 50 * 50;
int minInterval = fastMove ? 33 : 16;
int minDistSq = fastMove ? 10 * 10 : 3 * 3;
if (time_elapsed < minInterval && dist_sq < minDistSq) {
return;
}
m_lastMouseMove = now;
m_lastMousePoint = Msg->pt;
}
const int length = sizeof(MSG64) + 1;
BYTE szData[length + 3];
szData[0] = COMMAND_SCREEN_CONTROL;
memcpy(szData + 1, Msg, sizeof(MSG64));
szData[length] = 0;
m_ContextObject->Send2Client(szData, length);
}
void CScreenSpyDlg::SaveSnapshot(void)
{
auto path = GetScreenShotPath(this, m_IPAddress, _TR("位图文件(*.bmp)|*.bmp|"), "bmp");
if (path.empty())
return;
if (!WriteBitmap(m_BitmapInfor_Full, m_BitmapData_Full, path.c_str()))
return;
m_strSaveNotice = path;
m_nSaveNoticeTime = GetTickCount64();
}
VOID CScreenSpyDlg::UpdateServerClipboard(char* szBuffer, ULONG ulLength)
{
if (!::OpenClipboard(NULL))
return;
::EmptyClipboard();
// UTF-8 转 Unicode
int wlen = MultiByteToWideChar(CP_UTF8, 0, szBuffer, ulLength, nullptr, 0);
if (wlen > 0) {
// 分配 Unicode 缓冲区(+1 确保 null 结尾)
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, (wlen + 1) * sizeof(wchar_t));
if (hGlobal != NULL) {
wchar_t* pWideStr = (wchar_t*)GlobalLock(hGlobal);
if (pWideStr) {
MultiByteToWideChar(CP_UTF8, 0, szBuffer, ulLength, pWideStr, wlen);
pWideStr[wlen] = L'\0'; // 确保 null 结尾
GlobalUnlock(hGlobal);
if (SetClipboardData(CF_UNICODETEXT, hGlobal) == NULL) {
GlobalFree(hGlobal);
}
} else {
GlobalFree(hGlobal);
}
}
}
CloseClipboard();
}
VOID CScreenSpyDlg::SendServerClipboard(void)
{
if (!::OpenClipboard(NULL)) //打开剪切板设备
return;
HGLOBAL hGlobal = GetClipboardData(CF_TEXT); //代表着一个内存
if (hGlobal == NULL) {
::CloseClipboard();
return;
}
int iPacketLength = GlobalSize(hGlobal) + 1;
char* szClipboardVirtualAddress = (LPSTR) GlobalLock(hGlobal); //锁定
LPBYTE szBuffer = new BYTE[iPacketLength];
szBuffer[0] = COMMAND_SCREEN_SET_CLIPBOARD;
memcpy(szBuffer + 1, szClipboardVirtualAddress, iPacketLength - 1);
::GlobalUnlock(hGlobal);
::CloseClipboard();
m_ContextObject->Send2Client((PBYTE)szBuffer, iPacketLength);
delete[] szBuffer;
}
void CScreenSpyDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
if (m_bAdaptiveSize) return;
SCROLLINFO si = {sizeof(si)};
si.fMask = SIF_ALL;
GetScrollInfo(SB_HORZ, &si);
int nPrevPos = si.nPos;
switch(nSBCode) {
case SB_LEFT:
si.nPos = si.nMin;
break;
case SB_RIGHT:
si.nPos = si.nMax;
break;
case SB_LINELEFT:
si.nPos -= 8;
break;
case SB_LINERIGHT:
si.nPos += 8;
break;
case SB_PAGELEFT:
si.nPos -= si.nPage;
break;
case SB_PAGERIGHT:
si.nPos += si.nPage;
break;
case SB_THUMBTRACK:
si.nPos = si.nTrackPos;
break;
default:
break;
}
si.fMask = SIF_POS;
SetScrollInfo(SB_HORZ, &si, TRUE);
if (si.nPos != nPrevPos) {
m_ulHScrollPos += si.nPos - nPrevPos;
ScrollWindow(nPrevPos - si.nPos, 0, NULL, NULL);
}
__super::OnHScroll(nSBCode, nPrevPos, pScrollBar);
}
void CScreenSpyDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
if (m_bAdaptiveSize) return;
SCROLLINFO si = {sizeof(si)};
si.fMask = SIF_ALL;
GetScrollInfo(SB_VERT, &si);
int nPrevPos = si.nPos;
switch(nSBCode) {
case SB_TOP:
si.nPos = si.nMin;
break;
case SB_BOTTOM:
si.nPos = si.nMax;
break;
case SB_LINEUP:
si.nPos -= 8;
break;
case SB_LINEDOWN:
si.nPos += 8;
break;
case SB_PAGEUP:
si.nPos -= si.nPage;
break;
case SB_PAGEDOWN:
si.nPos += si.nPage;
break;
case SB_THUMBTRACK:
si.nPos = si.nTrackPos;
break;
default:
break;
}
si.fMask = SIF_POS;
SetScrollInfo(SB_VERT, &si, TRUE);
if (si.nPos != nPrevPos) {
m_ulVScrollPos += si.nPos - nPrevPos;
ScrollWindow(0, nPrevPos - si.nPos, NULL, NULL);
}
__super::OnVScroll(nSBCode, nPrevPos, pScrollBar);
}
void CScreenSpyDlg::EnterFullScreen()
{
if (1) {
// 1. 获取对话框当前所在的显示器
HMONITOR hMonitor = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi = { sizeof(mi) };
if (!GetMonitorInfo(hMonitor, &mi))
return;
RECT rcMonitor = mi.rcMonitor;
// 3. 记录当前窗口状态
GetWindowPlacement(&m_struOldWndpl);
// 4. 修改窗口样式,移除标题栏、边框
LONG lStyle = GetWindowLong(m_hWnd, GWL_STYLE);
lStyle &= ~(WS_CAPTION | WS_THICKFRAME | WS_BORDER);
SetWindowLong(m_hWnd, GWL_STYLE, lStyle);
// 5. 全屏无条件隐藏滚动条
ShowScrollBar(SB_BOTH, FALSE);
// 6. 重新调整窗口大小并更新
SetWindowPos(&CWnd::wndTop, rcMonitor.left, rcMonitor.top, rcMonitor.right - rcMonitor.left,
rcMonitor.bottom - rcMonitor.top, SWP_NOZORDER | SWP_FRAMECHANGED);
if (!m_pToolbar) {
m_pToolbar = new CToolbarDlg(this);
m_pToolbar->Create(IDD_TOOLBAR_DLG, this);
// OnInitDialog() 会根据 m_bLocked 和 m_nPosition 设置正确的位置和可见性
// 如果未锁定隐藏在屏幕边缘尺寸为1像素不跑到相邻屏幕
if (!m_pToolbar->m_bLocked) {
int monWidth = rcMonitor.right - rcMonitor.left;
int monHeight = rcMonitor.bottom - rcMonitor.top;
int hw = 480; // 水平工具栏宽度
int vh = 480; // 垂直工具栏高度
int hx = rcMonitor.left + (monWidth - hw) / 2;
switch (m_pToolbar->m_nPosition) {
case 0:
m_pToolbar->SetWindowPos(&wndTopMost, hx, rcMonitor.top, hw, 1, SWP_HIDEWINDOW);
break;
case 1:
m_pToolbar->SetWindowPos(&wndTopMost, hx, rcMonitor.bottom - 1, hw, 1, SWP_HIDEWINDOW);
break;
case 2: {
int vy = rcMonitor.top + (monHeight - vh) / 2;
m_pToolbar->SetWindowPos(&wndTopMost, rcMonitor.left, vy, 1, vh, SWP_HIDEWINDOW);
break;
}
case 3: {
int vy = rcMonitor.top + (monHeight - vh) / 2;
m_pToolbar->SetWindowPos(&wndTopMost, rcMonitor.right - 1, vy, 1, vh, SWP_HIDEWINDOW);
break;
}
}
}
// 同步系统菜单的锁定输入状态到工具栏
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu) {
BOOL bBlockInput = pSysMenu->GetMenuState(IDM_BLOCK_INPUT, MF_BYCOMMAND) & MF_CHECKED;
m_pToolbar->m_bBlockInput = (bBlockInput != 0);
m_pToolbar->UpdateButtonIcons();
}
}
// 创建状态信息窗口
if (!m_pStatusInfoWnd) {
m_pStatusInfoWnd = new CStatusInfoWnd();
m_pStatusInfoWnd->Create(this);
}
m_pStatusInfoWnd->UpdatePosition(rcMonitor);
// 同步工具栏透明度
if (m_pToolbar) {
m_pStatusInfoWnd->SetOpacityLevel(m_pToolbar->m_nOpacityLevel);
}
if (m_pToolbar && m_pToolbar->m_bShowStatusInfo) {
m_pStatusInfoWnd->Show();
}
// 7. 标记全屏模式
m_Settings.FullScreen = true;
SetTimer(1, 50, NULL);
}
}
// 全屏退出成功则返回true
bool CScreenSpyDlg::LeaveFullScreen()
{
if (1) {
KillTimer(1);
if (m_pToolbar) {
m_pToolbar->DestroyWindow();
delete m_pToolbar;
m_pToolbar = nullptr;
}
// 隐藏状态信息窗口
if (m_pStatusInfoWnd) {
m_pStatusInfoWnd->Hide();
}
// 1. 恢复窗口样式
LONG lStyle = GetWindowLong(m_hWnd, GWL_STYLE);
lStyle |= (WS_CAPTION | WS_THICKFRAME | WS_BORDER);
SetWindowLong(m_hWnd, GWL_STYLE, lStyle);
// 2. 恢复窗口大小
SetWindowPlacement(&m_struOldWndpl);
SetWindowPos(&CWnd::wndTop, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED);
// 3. 显示滚动条(全屏期间可能发生了质量切换,需要补设范围)
if (!m_bAdaptiveSize && m_BitmapInfor_Full) {
SetScrollRange(SB_HORZ, 0, m_BitmapInfor_Full->bmiHeader.biWidth);
SetScrollRange(SB_VERT, 0, m_BitmapInfor_Full->bmiHeader.biHeight);
}
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
// 4. 标记退出全屏
m_Settings.FullScreen = false;
CMenu* SysMenu = GetSystemMenu(FALSE);
SysMenu->CheckMenuItem(IDM_FULLSCREEN, MF_UNCHECKED); //菜单样式
return true;
}
return false;
}
void CScreenSpyDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
__super::OnLButtonDown(nFlags, point);
}
void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
__super::OnLButtonUp(nFlags, point);
}
BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
return __super::OnMouseWheel(nFlags, zDelta, pt);
}
void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
{
if (m_Settings.RemoteCursor) {
if (m_pToolbar != NULL && ::IsWindow(m_pToolbar->m_hWnd) && m_pToolbar->IsWindowVisible()) {
CRect rcToolbar;
m_pToolbar->GetWindowRect(&rcToolbar);
ScreenToClient(&rcToolbar); // 转换到主窗口坐标系
if (rcToolbar.PtInRect(point)) {
// 如果鼠标在工具栏区域,直接显示本地光标并返回,不发送远程指令
::SetCursor(LoadCursor(NULL, IDC_ARROW));
return;
}
}
if (m_bIsCtrl) {
// 关键:在控制模式下,强制设置光标为空,隐藏本地物理箭头
::SetCursor(NULL);
}
} else if (!m_bMouseTracking) {
m_bMouseTracking = true;
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
}
__super::OnMouseMove(nFlags, point);
}
void CScreenSpyDlg::OnMouseLeave()
{
CWnd::OnMouseLeave();
m_bMouseTracking = false;
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
}
void CScreenSpyDlg::OnKillFocus(CWnd* pNewWnd)
{
__super::OnKillFocus(pNewWnd);
// 清理远程修饰键状态,防止 Alt/Ctrl/Shift 卡住
// 场景:用户按住 Alt 后切换窗口KEYUP 不会发送到远程,导致修饰键卡住
if (m_bIsCtrl && m_ContextObject) {
static const WORD modifierKeys[] = { VK_MENU, VK_CONTROL, VK_SHIFT, VK_LWIN, VK_RWIN };
for (WORD vk : modifierKeys) {
MYMSG msg;
memset(&msg, 0, sizeof(msg));
msg.message = WM_KEYUP;
msg.wParam = vk;
msg.lParam = (MapVirtualKey(vk, MAPVK_VK_TO_VSC) << 16) | 0xC0000001; // 扫描码 + 释放标志
SendCommand(&msg);
}
}
}
void CScreenSpyDlg::OnSize(UINT nType, int cx, int cy)
{
__super::OnSize(nType, cx, cy);
// TODO: Add your message handler code here
if (!IsWindowVisible())
return;
GetClientRect(&m_CRect);
m_wZoom = ((double)m_BitmapInfor_Full->bmiHeader.biWidth) / ((double)(m_CRect.Width()));
m_hZoom = ((double)m_BitmapInfor_Full->bmiHeader.biHeight) / ((double)(m_CRect.Height()));
}
void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
{
CDialogBase::OnActivate(nState, pWndOther, bMinimized);
CWnd* pMain = AfxGetMainWnd();
if (!pMain)
return;
if (nState != WA_INACTIVE) {
// 通知主窗口:远程窗口获得焦点
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
}
}
void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
{
m_bIsCtrl = ctrl;
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
// 控制模式:禁用本地 IME查看模式启用本地 IME
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
}
void CScreenSpyDlg::OnDropFiles(HDROP hDropInfo)
{
if (m_bIsCtrl && m_bConnected) {
UINT nFiles = DragQueryFile(hDropInfo, 0xFFFFFFFF, NULL, 0);
std::vector<std::string> list;
for (UINT i = 0; i < nFiles; i++) {
TCHAR szPath[MAX_PATH];
DragQueryFile(hDropInfo, i, szPath, MAX_PATH);
list.push_back(szPath);
}
std::string GetPwdHash();
std::string GetHMAC(int offset);
auto files = PreprocessFilesSimple(list);
// 检查客户端是否支持 V2
uint64_t clientID = GetClientID();
context* mainCtx = m_pParent->FindHost(clientID);
if (mainCtx && SupportsFileTransferV2(mainCtx)) {
// V2 传输
m_pParent->SendFilesToClientV2(mainCtx, files);
Mprintf("【拖拽】 [本地 -> 远程] V2 传输: %zu 个文件\n", files.size());
} else {
// V1 传输(兼容旧客户端)
auto str = BuildMultiStringPath(files);
BYTE* szBuffer = new BYTE[1 + 80 + str.size()];
szBuffer[0] = { COMMAND_GET_FOLDER };
std::string masterId = GetPwdHash(), hmac = GetHMAC(100);
memcpy((char*)szBuffer + 1, masterId.c_str(), masterId.length());
memcpy((char*)szBuffer + 1 + masterId.length(), hmac.c_str(), hmac.length());
memcpy(szBuffer + 1 + 80, str.data(), str.size());
auto md5 = CalcMD5FromBytes((BYTE*)str.data(), str.size());
m_pParent->m_CmdList.PutCmd(md5);
m_ContextObject->Send2Client(szBuffer, 81 + str.size());
Mprintf("【拖拽】 [本地 -> 远程] V1 传输: %s\n", md5.c_str());
SAFE_DELETE_ARRAY(szBuffer);
}
}
DragFinish(hDropInfo);
CDialogBase::OnDropFiles(hDropInfo);
}
// ========== 音频播放实现 ==========
void CScreenSpyDlg::SendAudioCtrl(BYTE enable, BYTE persist)
{
AudioCtrlCmd cmd;
cmd.cmd = CMD_AUDIO_CTRL;
cmd.enable = enable;
cmd.persist = persist;
m_ContextObject->Send2Client((LPBYTE)&cmd, sizeof(cmd));
Mprintf("[ScreenSpy] 发送音频控制: enable=%d, persist=%d\n", enable, persist);
}
#if USING_OPUS
#include "../../compress/opus/opus_wrapper.h"
#endif
BOOL CScreenSpyDlg::InitAudioPlayback(const AudioFormat* fmt)
{
if (m_bAudioPlaying) return TRUE;
// 保存压缩类型
m_nAudioCompression = fmt->compression;
// 设置波形格式 (waveOut 始终使用 PCM)
memset(&m_AudioFormat, 0, sizeof(m_AudioFormat));
m_AudioFormat.wFormatTag = WAVE_FORMAT_PCM;
m_AudioFormat.nChannels = fmt->channels;
m_AudioFormat.nSamplesPerSec = fmt->sampleRate;
m_AudioFormat.wBitsPerSample = fmt->bitsPerSample;
m_AudioFormat.nBlockAlign = fmt->blockAlign;
m_AudioFormat.nAvgBytesPerSec = fmt->sampleRate * fmt->blockAlign;
m_AudioFormat.cbSize = 0;
// 打开波形输出设备 (使用 CALLBACK_WINDOW 接收播放完成通知)
MMRESULT result = waveOutOpen(&m_hWaveOut, WAVE_MAPPER, &m_AudioFormat,
(DWORD_PTR)m_hWnd, 0, CALLBACK_WINDOW);
if (result != MMSYSERR_NOERROR) {
Mprintf("[ScreenSpy] waveOutOpen 失败: %d\n", result);
return FALSE;
}
// 分配 waveOut 缓冲区
for (int i = 0; i < AUDIO_BUFFER_COUNT; i++) {
m_pAudioBuf[i] = new BYTE[AUDIO_BUF_SIZE];
memset(&m_WaveHdr[i], 0, sizeof(WAVEHDR));
m_WaveHdr[i].lpData = (LPSTR)m_pAudioBuf[i];
m_WaveHdr[i].dwBufferLength = AUDIO_BUF_SIZE;
waveOutPrepareHeader(m_hWaveOut, &m_WaveHdr[i], sizeof(WAVEHDR));
}
// 分配环形缓冲区
m_pRingBuf = new BYTE[RING_BUF_SIZE];
m_nRingHead = m_nRingTail = m_nRingDataLen = 0;
m_nPrebufferCount = 0;
#if USING_OPUS
// 初始化 Opus 解码器
if (m_nAudioCompression == AUDIO_COMPRESS_OPUS) {
COpusDecoder* pDecoder = new COpusDecoder();
if (pDecoder->Init(fmt->sampleRate, fmt->channels)) {
m_pOpusDecoder = pDecoder;
m_pOpusDecodeBuffer = new short[960 * 2 * 2]; // 最大 960 samples * 2 channels * 2 (安全余量)
Mprintf("[ScreenSpy] Opus 解码器初始化成功\n");
} else {
delete pDecoder;
Mprintf("[ScreenSpy] Opus 解码器初始化失败\n");
}
}
#endif
m_nAudioBufIndex = 0;
m_bAudioPlaying = TRUE;
Mprintf("[ScreenSpy] 音频播放已初始化: %d Hz, %d ch, %d bit, 压缩=%d\n",
fmt->sampleRate, fmt->channels, fmt->bitsPerSample, fmt->compression);
return TRUE;
}
void CScreenSpyDlg::StopAudioPlayback()
{
if (!m_bAudioPlaying) return;
m_bAudioPlaying = FALSE;
if (m_hWaveOut) {
waveOutReset(m_hWaveOut);
for (int i = 0; i < AUDIO_BUFFER_COUNT; i++) {
if (m_pAudioBuf[i]) {
waveOutUnprepareHeader(m_hWaveOut, &m_WaveHdr[i], sizeof(WAVEHDR));
delete[] m_pAudioBuf[i];
m_pAudioBuf[i] = nullptr;
}
}
waveOutClose(m_hWaveOut);
m_hWaveOut = NULL;
}
// 清理环形缓冲区
if (m_pRingBuf) {
delete[] m_pRingBuf;
m_pRingBuf = nullptr;
}
m_nRingHead = m_nRingTail = m_nRingDataLen = 0;
m_nPrebufferCount = 0;
#if USING_OPUS
// 清理 Opus 解码器
if (m_pOpusDecoder) {
delete (COpusDecoder*)m_pOpusDecoder;
m_pOpusDecoder = nullptr;
}
if (m_pOpusDecodeBuffer) {
delete[] m_pOpusDecodeBuffer;
m_pOpusDecodeBuffer = nullptr;
}
#endif
m_nAudioCompression = 0;
Mprintf("[ScreenSpy] 音频播放已停止\n");
}
void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len)
{
if (len < 1) return;
// 包计数(仅用于调试,可移除)
// static int s_audioPacketCount = 0;
// if (++s_audioPacketCount <= 5 || s_audioPacketCount % 500 == 0) {
// Mprintf("[Audio] 收到音频包 #%d, len=%u\n", s_audioPacketCount, len);
// }
BYTE hasFormat = pData[0];
UINT32 offset = 1;
// 如果带有格式信息,初始化播放
if (hasFormat && len >= 1 + sizeof(AudioFormat)) {
AudioFormat* fmt = (AudioFormat*)(pData + 1);
if (!m_bAudioPlaying) {
InitAudioPlayback(fmt);
}
offset = 1 + sizeof(AudioFormat);
// 检测到音频数据,更新菜单状态
if (!m_Settings.AudioEnabled) {
m_Settings.AudioEnabled = TRUE;
CMenu* SysMenu = GetSystemMenu(FALSE);
if (SysMenu) {
SysMenu->CheckMenuItem(IDM_AUDIO_TOGGLE, MF_CHECKED);
}
}
}
if (!m_bAudioPlaying || !m_hWaveOut || !m_pRingBuf) return;
// 获取音频数据
BYTE* pAudioData = pData + offset;
UINT32 audioLen = len - offset;
if (audioLen == 0) return;
// 帧对齐参数
DWORD blockAlign = m_AudioFormat.nBlockAlign;
if (blockAlign == 0) blockAlign = 4; // 默认 stereo 16-bit
#if USING_OPUS
// Opus 解码
if (m_nAudioCompression == AUDIO_COMPRESS_OPUS && m_pOpusDecoder && m_pOpusDecodeBuffer) {
COpusDecoder* pDecoder = (COpusDecoder*)m_pOpusDecoder;
int decodedSamples = pDecoder->Decode(pAudioData, audioLen, m_pOpusDecodeBuffer, 960 * 2);
if (decodedSamples > 0) {
pAudioData = (BYTE*)m_pOpusDecodeBuffer;
audioLen = decodedSamples * m_AudioFormat.nChannels * sizeof(short);
} else {
return; // 解码失败,丢弃此包
}
}
#endif
// 帧对齐:确保数据量是 blockAlign 的整数倍
audioLen = (audioLen / blockAlign) * blockAlign;
if (audioLen == 0) return;
// 检查环形缓冲区是否有足够空间
DWORD freeSpace = RING_BUF_SIZE - m_nRingDataLen;
if (audioLen > freeSpace) {
// 缓冲区满了,丢弃新数据(避免打断正在播放的音频)
audioLen = (freeSpace / blockAlign) * blockAlign;
if (audioLen == 0) return;
}
// 写入数据(可能需要分两次写入,处理环绕)
DWORD firstPart = min(audioLen, RING_BUF_SIZE - m_nRingHead);
memcpy(m_pRingBuf + m_nRingHead, pAudioData, firstPart);
if (audioLen > firstPart) {
memcpy(m_pRingBuf, pAudioData + firstPart, audioLen - firstPart);
}
m_nRingHead = (m_nRingHead + audioLen) % RING_BUF_SIZE;
m_nRingDataLen += audioLen;
// 预缓冲:等待积累足够数据再开始播放
if (m_nPrebufferCount < PREBUFFER_TARGET) {
m_nPrebufferCount++;
if (m_nPrebufferCount < PREBUFFER_TARGET) {
return; // 还没积累够,等待
}
Mprintf("[Audio] 预缓冲完成,开始播放 (缓冲: %u bytes)\n", m_nRingDataLen);
}
// 填充可用的 waveOut 缓冲区
FeedAudioBuffers();
}
void CScreenSpyDlg::FeedAudioBuffers()
{
if (!m_bAudioPlaying || !m_hWaveOut || !m_pRingBuf) return;
// 帧对齐参数
DWORD blockAlign = m_AudioFormat.nBlockAlign;
if (blockAlign == 0) blockAlign = 4; // 默认 stereo 16-bit
// 尽可能填满所有空闲的 waveOut 缓冲区
for (int i = 0; i < AUDIO_BUFFER_COUNT; i++) {
int bufIdx = (m_nAudioBufIndex + i) % AUDIO_BUFFER_COUNT;
WAVEHDR* pHdr = &m_WaveHdr[bufIdx];
// 如果缓冲区正在使用,跳过
if (pHdr->dwFlags & WHDR_INQUEUE) continue;
// 检查环形缓冲区是否有足够数据(至少一帧)
if (m_nRingDataLen < blockAlign) break;
// 计算要读取的数据量(对齐到帧边界)
DWORD readLen = min(m_nRingDataLen, (DWORD)AUDIO_BUF_SIZE);
readLen = (readLen / blockAlign) * blockAlign; // 截断到帧边界
if (readLen == 0) break;
// 从环形缓冲区读取(可能需要分两次读取,处理环绕)
DWORD firstPart = min(readLen, RING_BUF_SIZE - m_nRingTail);
memcpy(m_pAudioBuf[bufIdx], m_pRingBuf + m_nRingTail, firstPart);
if (readLen > firstPart) {
memcpy(m_pAudioBuf[bufIdx] + firstPart, m_pRingBuf, readLen - firstPart);
}
m_nRingTail = (m_nRingTail + readLen) % RING_BUF_SIZE;
m_nRingDataLen -= readLen;
// 播放
pHdr->dwBufferLength = readLen;
MMRESULT mr = waveOutWrite(m_hWaveOut, pHdr, sizeof(WAVEHDR));
if (mr != MMSYSERR_NOERROR) {
Mprintf("[Audio] waveOutWrite 失败: %d\n", mr);
}
}
// 更新下一个缓冲区索引(找到第一个空闲的)
for (int i = 0; i < AUDIO_BUFFER_COUNT; i++) {
if (!(m_WaveHdr[m_nAudioBufIndex].dwFlags & WHDR_INQUEUE)) break;
m_nAudioBufIndex = (m_nAudioBufIndex + 1) % AUDIO_BUFFER_COUNT;
}
}
LRESULT CScreenSpyDlg::OnWaveOutDone(WPARAM wParam, LPARAM lParam)
{
// waveOut 缓冲区播放完成,尝试填充更多数据
if (m_bAudioPlaying && m_nRingDataLen > 0) {
FeedAudioBuffers();
}
return 0;
}