3050 lines
108 KiB
C++
3050 lines
108 KiB
C++
// 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;
|
||
}
|