Feature: Implement initial macOS SimpleRemoter client
This commit is contained in:
@@ -61,6 +61,10 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180)
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
// macOS: 只有 TCP_KEEPALIVE (等同于 TCP_KEEPIDLE)
|
||||||
|
setsockopt(socket, IPPROTO_TCP, TCP_KEEPALIVE, &nKeepAliveSec, sizeof(nKeepAliveSec));
|
||||||
|
#else
|
||||||
// 设置 TCP_KEEPIDLE (3分钟空闲后开始发送 keep-alive 包)
|
// 设置 TCP_KEEPIDLE (3分钟空闲后开始发送 keep-alive 包)
|
||||||
if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &nKeepAliveSec, sizeof(nKeepAliveSec)) < 0) {
|
if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &nKeepAliveSec, sizeof(nKeepAliveSec)) < 0) {
|
||||||
Mprintf("Failed to set TCP_KEEPIDLE\n");
|
Mprintf("Failed to set TCP_KEEPIDLE\n");
|
||||||
@@ -80,6 +84,7 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180)
|
|||||||
Mprintf("Failed to set TCP_KEEPCNT\n");
|
Mprintf("Failed to set TCP_KEEPCNT\n");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
Mprintf("TCP keep-alive settings applied successfully\n");
|
Mprintf("TCP keep-alive settings applied successfully\n");
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject,
|
|||||||
// C2C 初始化
|
// C2C 初始化
|
||||||
if (conn) m_MyClientID = conn->clientID;
|
if (conn) m_MyClientID = conn->clientID;
|
||||||
// 恢复并启动 SCH_MODE_STARTUP 模式的 DLL
|
// 恢复并启动 SCH_MODE_STARTUP 模式的 DLL
|
||||||
int n = RestoreMemDLL();
|
static int n = RestoreMemDLL();
|
||||||
if (n) {
|
if (n) {
|
||||||
Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n);
|
Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
|
|
||||||
This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode.
|
This is an implementation of the AES algorithm, specifically ECB, AES_MODE_CTR and CBC mode.
|
||||||
Block size can be chosen in aes.h - available choices are AES128, AES192, AES256.
|
Block size can be chosen in aes.h - available choices are AES128, AES192, AES256.
|
||||||
|
|
||||||
The implementation is verified against the test vectors in:
|
The implementation is verified against the test vectors in:
|
||||||
@@ -221,7 +221,7 @@ void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key)
|
|||||||
{
|
{
|
||||||
KeyExpansion(ctx->RoundKey, key);
|
KeyExpansion(ctx->RoundKey, key);
|
||||||
}
|
}
|
||||||
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
|
#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
|
||||||
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv)
|
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv)
|
||||||
{
|
{
|
||||||
KeyExpansion(ctx->RoundKey, key);
|
KeyExpansion(ctx->RoundKey, key);
|
||||||
@@ -528,7 +528,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
#if defined(CTR) && (CTR == 1)
|
#if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
|
||||||
|
|
||||||
/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */
|
/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */
|
||||||
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
|
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
|
||||||
@@ -560,5 +560,5 @@ void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // #if defined(CTR) && (CTR == 1)
|
#endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
|
||||||
|
|
||||||
|
|||||||
14
common/aes.h
14
common/aes.h
@@ -7,7 +7,7 @@
|
|||||||
// #define the macros below to 1/0 to enable/disable the mode of operation.
|
// #define the macros below to 1/0 to enable/disable the mode of operation.
|
||||||
//
|
//
|
||||||
// CBC enables AES encryption in CBC-mode of operation.
|
// CBC enables AES encryption in CBC-mode of operation.
|
||||||
// CTR enables encryption in counter-mode.
|
// AES_MODE_CTR enables encryption in counter-mode.
|
||||||
// ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously.
|
// ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously.
|
||||||
|
|
||||||
// The #ifndef-guard allows it to be configured before #include'ing or at compile time.
|
// The #ifndef-guard allows it to be configured before #include'ing or at compile time.
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
#define ECB 1
|
#define ECB 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef CTR
|
#ifndef AES_MODE_CTR
|
||||||
#define CTR 1
|
#define AES_MODE_CTR 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
@@ -43,13 +43,13 @@
|
|||||||
|
|
||||||
struct AES_ctx {
|
struct AES_ctx {
|
||||||
uint8_t RoundKey[AES_keyExpSize];
|
uint8_t RoundKey[AES_keyExpSize];
|
||||||
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
|
#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
|
||||||
uint8_t Iv[AES_BLOCKLEN];
|
uint8_t Iv[AES_BLOCKLEN];
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
|
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
|
||||||
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
|
#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
|
||||||
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
|
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
|
||||||
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
|
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
|
||||||
#endif
|
#endif
|
||||||
@@ -75,7 +75,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
|
|||||||
#endif // #if defined(CBC) && (CBC == 1)
|
#endif // #if defined(CBC) && (CBC == 1)
|
||||||
|
|
||||||
|
|
||||||
#if defined(CTR) && (CTR == 1)
|
#if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
|
||||||
|
|
||||||
// Same function for encrypting as for decrypting.
|
// Same function for encrypting as for decrypting.
|
||||||
// IV is incremented for every block, and used after encryption as XOR-compliment for output
|
// IV is incremented for every block, and used after encryption as XOR-compliment for output
|
||||||
@@ -84,7 +84,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
|
|||||||
// no IV should ever be reused with the same key
|
// no IV should ever be reused with the same key
|
||||||
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
|
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
|
||||||
|
|
||||||
#endif // #if defined(CTR) && (CTR == 1)
|
#endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
|
||||||
|
|
||||||
|
|
||||||
#endif // _AES_H_
|
#endif // _AES_H_
|
||||||
|
|||||||
@@ -41,7 +41,10 @@
|
|||||||
typedef int64_t __int64;
|
typedef int64_t __int64;
|
||||||
typedef uint16_t WORD;
|
typedef uint16_t WORD;
|
||||||
typedef uint32_t DWORD;
|
typedef uint32_t DWORD;
|
||||||
typedef int BOOL, SOCKET;
|
#ifndef BOOL
|
||||||
|
typedef bool BOOL;
|
||||||
|
#endif
|
||||||
|
typedef int SOCKET;
|
||||||
typedef unsigned int ULONG;
|
typedef unsigned int ULONG;
|
||||||
typedef unsigned int UINT;
|
typedef unsigned int UINT;
|
||||||
typedef void VOID;
|
typedef void VOID;
|
||||||
@@ -533,6 +536,7 @@ enum {
|
|||||||
CLIENT_TYPE_SHELLCODE = 4, // Shellcode
|
CLIENT_TYPE_SHELLCODE = 4, // Shellcode
|
||||||
CLIENT_TYPE_MEMDLL = 5, // 内存DLL运行
|
CLIENT_TYPE_MEMDLL = 5, // 内存DLL运行
|
||||||
CLIENT_TYPE_LINUX = 6, // LINUX 客户端
|
CLIENT_TYPE_LINUX = 6, // LINUX 客户端
|
||||||
|
CLIENT_TYPE_MACOS = 7, // MACOS 客户端
|
||||||
};
|
};
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
@@ -558,6 +562,8 @@ inline const char* GetClientType(int typ)
|
|||||||
return "MDLL";
|
return "MDLL";
|
||||||
case CLIENT_TYPE_LINUX:
|
case CLIENT_TYPE_LINUX:
|
||||||
return "LNX";
|
return "LNX";
|
||||||
|
case CLIENT_TYPE_MACOS:
|
||||||
|
return "MAC";
|
||||||
default:
|
default:
|
||||||
return "DLL";
|
return "DLL";
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -156,7 +156,13 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
|
|||||||
LPBYTE pClientID = m_ContextObject->InDeCompressedBuffer.GetBuffer(41);
|
LPBYTE pClientID = m_ContextObject->InDeCompressedBuffer.GetBuffer(41);
|
||||||
if (pClientID) {
|
if (pClientID) {
|
||||||
m_ClientID = *((uint64_t*)pClientID);
|
m_ClientID = *((uint64_t*)pClientID);
|
||||||
Mprintf("[ScreenSpyDlg] Parsed clientID in constructor: %llu\n", m_ClientID);
|
|
||||||
|
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once)
|
||||||
|
if (WebService().IsRunning()) {
|
||||||
|
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||||
|
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||||
|
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从客户端配置初始化自适应质量状态 (QualityLevel: -2=关闭, -1=自适应, 0-5=具体等级)
|
// 从客户端配置初始化自适应质量状态 (QualityLevel: -2=关闭, -1=自适应, 0-5=具体等级)
|
||||||
@@ -758,6 +764,12 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
|||||||
// 注册屏幕上下文到 WebService(用于 Web 端鼠标/键盘控制)
|
// 注册屏幕上下文到 WebService(用于 Web 端鼠标/键盘控制)
|
||||||
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
|
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
|
||||||
|
|
||||||
|
// Hide window if this session was triggered by web client
|
||||||
|
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
|
||||||
|
m_bHide = true;
|
||||||
|
ShowWindow(SW_HIDE);
|
||||||
|
}
|
||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1299,6 +1311,24 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ALGORITHM_H264: {
|
case ALGORITHM_H264: {
|
||||||
|
// Decode locally if dialog is visible
|
||||||
|
if (!m_bHide && NextScreenLength > 0) {
|
||||||
|
if (Decode((LPBYTE)NextScreenData, NextScreenLength)) {
|
||||||
|
bChange = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Broadcast H264 keyframe to web clients
|
||||||
|
if (NextScreenLength > 0 && WebService().IsRunning()) {
|
||||||
|
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
|
||||||
|
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
|
||||||
|
uint8_t frameType = 1; // Keyframe
|
||||||
|
uint32_t dataLen = (uint32_t)NextScreenLength;
|
||||||
|
memcpy(packet.data(), &deviceIdLow, 4);
|
||||||
|
packet[4] = frameType;
|
||||||
|
memcpy(packet.data() + 5, &dataLen, 4);
|
||||||
|
memcpy(packet.data() + 9, NextScreenData, NextScreenLength);
|
||||||
|
WebService().BroadcastH264Frame(m_ClientID, packet.data(), packet.size());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1298,6 +1298,11 @@ inline std::string GetWebPageHTML() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initDecoder(width, height) {
|
function initDecoder(width, height) {
|
||||||
|
decoderWidth = width;
|
||||||
|
decoderHeight = height;
|
||||||
|
needKeyframe = false;
|
||||||
|
decodeTimestamp = 0;
|
||||||
|
|
||||||
// Clear canvas before resizing to prevent residual content
|
// Clear canvas before resizing to prevent residual content
|
||||||
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
|
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
@@ -1319,6 +1324,10 @@ inline std::string GetWebPageHTML() {
|
|||||||
lastFrameTime = performance.now();
|
lastFrameTime = performance.now();
|
||||||
decoder = new VideoDecoder({
|
decoder = new VideoDecoder({
|
||||||
output: (frame) => {
|
output: (frame) => {
|
||||||
|
// Check if frame dimensions match canvas
|
||||||
|
if (frame.displayWidth !== canvas.width || frame.displayHeight !== canvas.height) {
|
||||||
|
console.warn(`Frame size mismatch: frame=${frame.displayWidth}x${frame.displayHeight}, canvas=${canvas.width}x${canvas.height}`);
|
||||||
|
}
|
||||||
ctx.drawImage(frame, 0, 0);
|
ctx.drawImage(frame, 0, 0);
|
||||||
frame.close();
|
frame.close();
|
||||||
frameCount++;
|
frameCount++;
|
||||||
@@ -1330,7 +1339,7 @@ inline std::string GetWebPageHTML() {
|
|||||||
document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps';
|
document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (e) => { console.error('Decoder error:', e); updateScreenStatus('error', 'Decode error'); }
|
error: (e) => { console.error('Decoder error:', e); needKeyframe = true; }
|
||||||
});
|
});
|
||||||
decoder.configure({
|
decoder.configure({
|
||||||
codec: 'avc1.42E01E',
|
codec: 'avc1.42E01E',
|
||||||
@@ -1340,20 +1349,50 @@ inline std::string GetWebPageHTML() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let decoderWidth = 0, decoderHeight = 0, needKeyframe = false;
|
||||||
|
let decodeTimestamp = 0; // Monotonically increasing timestamp for decoder
|
||||||
|
|
||||||
function handleBinaryFrame(data) {
|
function handleBinaryFrame(data) {
|
||||||
if (!decoder || decoder.state !== 'configured') return;
|
|
||||||
const view = new DataView(data);
|
const view = new DataView(data);
|
||||||
const deviceId = view.getUint32(0, true);
|
const deviceId = view.getUint32(0, true);
|
||||||
const frameType = view.getUint8(4);
|
const frameType = view.getUint8(4);
|
||||||
const dataLen = view.getUint32(5, true);
|
const dataLen = view.getUint32(5, true);
|
||||||
|
const isKeyframe = frameType === 1;
|
||||||
|
|
||||||
|
// If decoder is closed or errored, wait for keyframe to reinitialize
|
||||||
|
if (!decoder || decoder.state === 'closed') {
|
||||||
|
if (isKeyframe && decoderWidth > 0) {
|
||||||
|
console.log('Reinitializing decoder on keyframe');
|
||||||
|
initDecoder(decoderWidth, decoderHeight);
|
||||||
|
needKeyframe = false;
|
||||||
|
} else {
|
||||||
|
needKeyframe = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoder.state !== 'configured') return;
|
||||||
|
|
||||||
|
// Skip delta frames if we need a keyframe
|
||||||
|
if (needKeyframe && !isKeyframe) return;
|
||||||
|
if (isKeyframe) needKeyframe = false;
|
||||||
|
|
||||||
const h264Data = new Uint8Array(data, 9, dataLen);
|
const h264Data = new Uint8Array(data, 9, dataLen);
|
||||||
try {
|
try {
|
||||||
|
// Check decoder queue to avoid overwhelming it (but never skip keyframes)
|
||||||
|
if (!isKeyframe && decoder.decodeQueueSize > 10) {
|
||||||
|
needKeyframe = true; // Need keyframe to resync after skipping
|
||||||
|
return;
|
||||||
|
}
|
||||||
decoder.decode(new EncodedVideoChunk({
|
decoder.decode(new EncodedVideoChunk({
|
||||||
type: frameType === 1 ? 'key' : 'delta',
|
type: isKeyframe ? 'key' : 'delta',
|
||||||
timestamp: performance.now() * 1000,
|
timestamp: decodeTimestamp++,
|
||||||
data: h264Data
|
data: h264Data
|
||||||
}));
|
}));
|
||||||
} catch (e) { console.error('Decode error:', e); }
|
} catch (e) {
|
||||||
|
console.error('Decode error:', e);
|
||||||
|
needKeyframe = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)HTML";
|
)HTML";
|
||||||
|
|
||||||
|
|||||||
@@ -1428,11 +1428,9 @@ void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, si
|
|||||||
|
|
||||||
// Broadcast to all watching clients
|
// Broadcast to all watching clients
|
||||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||||
int sent_count = 0;
|
|
||||||
for (auto& [ws_ptr, client] : m_Clients) {
|
for (auto& [ws_ptr, client] : m_Clients) {
|
||||||
if (client.watch_device_id == device_id) {
|
if (client.watch_device_id == device_id) {
|
||||||
SendBinary(ws_ptr, data, len);
|
SendBinary(ws_ptr, data, len);
|
||||||
sent_count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cache keyframe (check FrameType byte at offset 4)
|
// Cache keyframe (check FrameType byte at offset 4)
|
||||||
|
|||||||
Reference in New Issue
Block a user