Feature: Implement initial macOS SimpleRemoter client
This commit is contained in:
73
macos/CMakeLists.txt
Normal file
73
macos/CMakeLists.txt
Normal file
@@ -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
|
||||
)
|
||||
86
macos/H264Encoder.h
Normal file
86
macos/H264Encoder.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
|
||||
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<uint8_t> m_yPlane;
|
||||
std::vector<uint8_t> m_uPlane;
|
||||
std::vector<uint8_t> m_vPlane;
|
||||
|
||||
// Output buffer
|
||||
std::vector<uint8_t> m_outputBuffer;
|
||||
std::mutex m_outputMutex;
|
||||
|
||||
// State
|
||||
std::atomic<bool> m_forceKeyframe;
|
||||
int64_t m_frameCount;
|
||||
char m_lastError[256];
|
||||
};
|
||||
521
macos/H264Encoder.mm
Normal file
521
macos/H264Encoder.mm
Normal file
@@ -0,0 +1,521 @@
|
||||
#import "H264Encoder.h"
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
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<std::mutex> 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<std::mutex> 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<std::mutex> 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;
|
||||
}
|
||||
}
|
||||
80
macos/InputHandler.h
Normal file
80
macos/InputHandler.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#include <cstdint>
|
||||
#include <atomic>
|
||||
|
||||
// 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<bool> m_hasPermission{false};
|
||||
std::atomic<bool> m_warningLogged{false};
|
||||
|
||||
// Track button states for CGEvent (atomic for thread safety)
|
||||
CGPoint m_lastMousePos;
|
||||
std::atomic<bool> m_leftButtonDown{false};
|
||||
std::atomic<bool> m_rightButtonDown{false};
|
||||
std::atomic<bool> m_middleButtonDown{false};
|
||||
|
||||
// Track modifier key states for proper key event handling
|
||||
std::atomic<CGEventFlags> m_modifierFlags{0};
|
||||
};
|
||||
399
macos/InputHandler.mm
Normal file
399
macos/InputHandler.mm
Normal file
@@ -0,0 +1,399 @@
|
||||
#import "InputHandler.h"
|
||||
#import "Permissions.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
#include <unistd.h> // 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
|
||||
}
|
||||
}
|
||||
36
macos/Permissions.h
Normal file
36
macos/Permissions.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
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);
|
||||
};
|
||||
64
macos/Permissions.mm
Normal file
64
macos/Permissions.mm
Normal file
@@ -0,0 +1,64 @@
|
||||
#import "Permissions.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
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;
|
||||
}
|
||||
61
macos/README.txt
Normal file
61
macos/README.txt
Normal file
@@ -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
|
||||
129
macos/ScreenHandler.h
Normal file
129
macos/ScreenHandler.h
Normal file
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
#import "../client/IOCPClient.h"
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
// 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<uint8_t>& 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<bool> 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<uint8_t> m_prevFrame;
|
||||
std::vector<uint8_t> m_currFrame;
|
||||
std::vector<uint8_t> m_diffBuffer;
|
||||
|
||||
// Quality settings
|
||||
std::atomic<uint8_t> m_algorithm;
|
||||
std::atomic<int> m_maxFPS;
|
||||
int8_t m_qualityLevel;
|
||||
|
||||
// H264 encoder
|
||||
std::unique_ptr<H264Encoder> m_h264Encoder;
|
||||
int m_h264Bitrate;
|
||||
|
||||
// Input handler for mouse/keyboard control
|
||||
std::unique_ptr<InputHandler> m_inputHandler;
|
||||
};
|
||||
574
macos/ScreenHandler.mm
Normal file
574
macos/ScreenHandler.mm
Normal file
@@ -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 <Cocoa/Cocoa.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <mach/mach_time.h>
|
||||
|
||||
// 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<InputHandler>();
|
||||
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<uint8_t> 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<uint8_t>& 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<uint8_t> 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<uint8_t> 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<H264Encoder>();
|
||||
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<uint8_t> 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");
|
||||
}
|
||||
40
macos/SystemManager.h
Normal file
40
macos/SystemManager.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// 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<pid_t> getAllPids();
|
||||
|
||||
private:
|
||||
IOCPClient* m_client;
|
||||
uint64_t m_clientID;
|
||||
};
|
||||
201
macos/SystemManager.mm
Normal file
201
macos/SystemManager.mm
Normal file
@@ -0,0 +1,201 @@
|
||||
#import "SystemManager.h"
|
||||
#import "../client/IOCPClient.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <sys/sysctl.h>
|
||||
#import <libproc.h>
|
||||
#import <signal.h>
|
||||
#import <unistd.h>
|
||||
|
||||
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<pid_t> SystemManager::getAllPids()
|
||||
{
|
||||
std::vector<pid_t> 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<pid_t> 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<uint8_t> 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<pid_t> 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<uint8_t> buf;
|
||||
buf.push_back(TOKEN_WSLIST);
|
||||
|
||||
// Get list of running applications
|
||||
NSArray<NSRunningApplication*>* 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());
|
||||
}
|
||||
47
macos/build.sh
Normal file
47
macos/build.sh
Normal file
@@ -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"
|
||||
BIN
macos/lib/libzstd.a
Normal file
BIN
macos/lib/libzstd.a
Normal file
Binary file not shown.
548
macos/main.mm
Normal file
548
macos/main.mm
Normal file
@@ -0,0 +1,548 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <sys/sysctl.h>
|
||||
#import <mach/mach.h>
|
||||
#import <mach-o/dyld.h>
|
||||
#import <pwd.h>
|
||||
#import <signal.h>
|
||||
#import <unistd.h>
|
||||
#import <IOKit/IOKitLib.h>
|
||||
#import <fstream>
|
||||
#import <thread>
|
||||
#import <atomic>
|
||||
#import <memory>
|
||||
#import <string>
|
||||
|
||||
#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<bool> 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<FILE, decltype(&pclose)> 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<IOCPClient> 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<ScreenHandler> 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<IOCPClient>(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;
|
||||
}
|
||||
Reference in New Issue
Block a user