From ca37fa419a43bb7431f1142618c8733e30c8c19b Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sun, 3 May 2026 12:15:22 +0200 Subject: [PATCH] Feat: Implement H264 for Linux client with dynamic libx264 loading --- common/PTYHandler.h | 2 + common/commands.h | 8 + linux/ScreenHandler.h | 152 +++++++++++-- linux/X264Encoder.h | 471 +++++++++++++++++++++++++++++++++++++++++ macos/ScreenHandler.h | 7 +- macos/ScreenHandler.mm | 44 ++-- 6 files changed, 646 insertions(+), 38 deletions(-) create mode 100644 linux/X264Encoder.h diff --git a/common/PTYHandler.h b/common/PTYHandler.h index 8c275c6..980d7fb 100644 --- a/common/PTYHandler.h +++ b/common/PTYHandler.h @@ -202,6 +202,8 @@ private: // macOS locale settings setenv("LANG", "en_US.UTF-8", 1); setenv("LC_ALL", "en_US.UTF-8", 1); + // Disable zsh session save/restore (causes errors in PTY) + setenv("SHELL_SESSIONS_DISABLE", "1", 1); // Try zsh first (macOS default), fallback to bash if (access("/bin/zsh", X_OK) == 0) { diff --git a/common/commands.h b/common/commands.h index d2eae00..7d4991b 100644 --- a/common/commands.h +++ b/common/commands.h @@ -1048,6 +1048,14 @@ enum QualityLevel { QUALITY_COUNT = 6, }; +// 屏幕压缩算法常量 (所有平台共用) +#ifndef ALGORITHM_GRAY +#define ALGORITHM_GRAY 0 // 灰度压缩 +#define ALGORITHM_DIFF 1 // 差分压缩 (BGRA) +#define ALGORITHM_H264 2 // H264 硬件编码 +#define ALGORITHM_RGB565 3 // RGB565 压缩 +#endif + /* 质量配置(与 QualityLevel 对应) - strategy = 0:1080p 限制 - strategy = 1:原始分辨率 diff --git a/linux/ScreenHandler.h b/linux/ScreenHandler.h index b491435..7e58ad2 100644 --- a/linux/ScreenHandler.h +++ b/linux/ScreenHandler.h @@ -4,6 +4,7 @@ #include "LinuxConfig.h" #include "ClipboardHandler.h" #include "common/FileTransferV2.h" +#include "X264Encoder.h" #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include // 客户端 ID(定义在 main.cpp) extern uint64_t g_myClientID; @@ -110,27 +112,39 @@ struct XGCValues_LNX { #define IncludeInferiors 1 // ============== 屏幕算法常量 ============== -#define ALGORITHM_GRAY 0 -#define ALGORITHM_DIFF 1 -#define ALGORITHM_H264 2 -#define ALGORITHM_RGB565 3 +// 常量定义已移至 commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565 -// 算法支持表(编译时常量,日后支持 H264 时改为 true) -static const bool g_SupportedAlgo[] = { - true, // ALGORITHM_GRAY = 0 - true, // ALGORITHM_DIFF = 1 - false, // ALGORITHM_H264 = 2 - true, // ALGORITHM_RGB565 = 3 -}; +// 检查算法是否支持(H264 需要运行时检测) +inline bool IsAlgorithmSupported(uint8_t algo) { + switch (algo) { + case ALGORITHM_GRAY: + case ALGORITHM_DIFF: + case ALGORITHM_RGB565: + return true; + case ALGORITHM_H264: + return X264Encoder::IsAvailable(); + default: + return false; + } +} // 不支持的算法降级为 RGB565 inline uint8_t GetEffectiveAlgorithm(uint8_t algo) { - if (algo > 3 || !g_SupportedAlgo[algo]) { + if (!IsAlgorithmSupported(algo)) { + Mprintf(">>> Algorithm %d not supported, fallback to RGB565\n", algo); return ALGORITHM_RGB565; } return algo; } +// 码率到 CRF 映射 (参考 Windows/macOS 实现) +inline int BitRateToCRF(int bitrate) { + if (bitrate >= 3000) return 20; // 高质量 + if (bitrate >= 2000) return 23; // 中等 + if (bitrate >= 1200) return 26; // 较低 + return 30; // 最低 +} + // ============== 颜色转换函数 ============== // BGRA → 灰度 (Y = 0.299R + 0.587G + 0.114B) @@ -538,7 +552,8 @@ public: m_inputDisplay(nullptr), m_width(0), m_height(0), m_pixmap(0), m_gc(nullptr), m_xtestWarned(false), - m_bAlgorithm(ALGORITHM_DIFF), m_maxFPS(10), m_qualityLevel(QUALITY_ADAPTIVE) + m_bAlgorithm(ALGORITHM_DIFF), m_maxFPS(10), m_qualityLevel(QUALITY_ADAPTIVE), + m_h264Bitrate(2000) { if (!client) { throw std::invalid_argument("IOCPClient pointer cannot be null"); @@ -883,12 +898,28 @@ public: // 应用帧率 m_maxFPS.store(profile.maxFPS); + // 应用码率(H264 使用) + int oldBitrate = m_h264Bitrate; + m_h264Bitrate = profile.bitRate; + // 应用算法(带降级处理) uint8_t algo = GetEffectiveAlgorithm(profile.algorithm); + uint8_t oldAlgo = m_bAlgorithm.load(); m_bAlgorithm.store(algo); - Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d\n", - level, profile.maxFPS, profile.algorithm, algo); + // 如果 H264 参数变化,需要重新初始化编码器 + if (algo == ALGORITHM_H264 && oldAlgo == ALGORITHM_H264 && + (oldBitrate != m_h264Bitrate)) { + // 码率变化,重置编码器(下次编码时重新初始化) + if (m_h264Encoder) { + m_h264Encoder->close(); + m_h264Encoder.reset(); + Mprintf(">>> H264 encoder reset due to bitrate change\n"); + } + } + + Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d, Bitrate=%d\n", + level, profile.maxFPS, profile.algorithm, algo, profile.bitRate); } else { // 自适应模式 (level=-1):由服务端动态调整,不做处理 Mprintf(">>> Quality: Adaptive mode\n"); @@ -1054,11 +1085,15 @@ private: std::vector m_diffBuffer; // 自适应质量控制 - std::atomic m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY) + std::atomic m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY/H264) std::atomic m_maxFPS; // 最大帧率 int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级) LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf) + // H264 编码器 + std::unique_ptr m_h264Encoder; + int m_h264Bitrate; // 码率 (kbps) + // X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致) // 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap,再对 Pixmap 调用 XGetImage // 这样可以避免合成窗口管理器(Mutter 等)导致的 BadMatch 错误 @@ -1151,6 +1186,59 @@ private: std::swap(m_prevFrame, m_currFrame); } + // 发送 H264 编码帧 + void SendH264Frame(bool forceKeyframe = false) + { + if (!CaptureScreen(m_currFrame)) return; + if (!m_client) return; + + // 惰性初始化编码器 + if (!m_h264Encoder) { + m_h264Encoder.reset(new X264Encoder()); + int fps = m_maxFPS.load(); + if (fps <= 0) fps = 20; + int crf = BitRateToCRF(m_h264Bitrate); + if (!m_h264Encoder->open(m_bmpHeader.biWidth, m_bmpHeader.biHeight, fps, crf)) { + Mprintf("*** H264 encoder init failed, falling back to RGB565\n"); + m_bAlgorithm.store(ALGORITHM_RGB565); + m_h264Encoder.reset(); + SendDiffFrame(); + return; + } + Mprintf(">>> H264 encoder initialized: %dx%d @ %d fps, CRF=%d\n", + m_bmpHeader.biWidth, m_bmpHeader.biHeight, fps, crf); + } + + // 编码当前帧 + uint8_t* encodedData = nullptr; + uint32_t encodedSize = 0; + + // direction=1 表示 bottom-up (BMP 格式) + int result = m_h264Encoder->encode( + m_currFrame.data(), 32, m_bmpHeader.biWidth * 4, + m_bmpHeader.biWidth, m_bmpHeader.biHeight, + &encodedData, &encodedSize, 1); + + if (result != 0 || !encodedData || encodedSize == 0) { + return; + } + + // 构建数据包: [TOKEN_NEXTSCREEN][algo][cursorX][cursorY][cursorType][H264Data] + uint32_t headerSize = 1 + 1 + 2 * sizeof(int32_t) + 1; + std::vector packet(headerSize + encodedSize); + + packet[0] = TOKEN_NEXTSCREEN; + packet[1] = ALGORITHM_H264; + + int32_t cursorX = 0, cursorY = 0; + memcpy(&packet[2], &cursorX, sizeof(int32_t)); + memcpy(&packet[6], &cursorY, sizeof(int32_t)); + packet[10] = 0; // cursorType + + memcpy(&packet[headerSize], encodedData, encodedSize); + m_client->Send2Server((char*)packet.data(), packet.size()); + } + // 差异比较算法(支持 DIFF/RGB565/GRAY) // 输出格式: [byteOffset(4) + length(4) + pixel data] ... // DIFF: length = 字节数, data = BGRA 原始数据 @@ -1243,10 +1331,34 @@ private: // 发送第一帧 SendFirstScreen(); + uint8_t currentAlgo = m_bAlgorithm.load(); + while (m_running) { uint64_t start = GetTickMs(); + uint8_t algo = m_bAlgorithm.load(); - SendDiffFrame(); + // 算法切换处理 + if (algo != currentAlgo) { + currentAlgo = algo; + if (algo == ALGORITHM_H264) { + // 切换到 H264,发送关键帧 + SendH264Frame(true); + } else { + // 切换离开 H264,关闭编码器并发送完整帧 + if (m_h264Encoder) { + m_h264Encoder->close(); + m_h264Encoder.reset(); + } + SendFirstScreen(); + } + } else { + // 正常帧 + if (algo == ALGORITHM_H264) { + SendH264Frame(false); + } else { + SendDiffFrame(); + } + } // 动态计算帧间隔(根据当前 maxFPS) int fps = m_maxFPS.load(); @@ -1260,6 +1372,12 @@ private: } } + // 清理 H264 编码器 + if (m_h264Encoder) { + m_h264Encoder->close(); + m_h264Encoder.reset(); + } + Mprintf(">>> ScreenHandler CaptureLoop stopped\n"); } catch (const std::exception& e) { Mprintf("*** CaptureLoop exception: %s ***\n", e.what()); diff --git a/linux/X264Encoder.h b/linux/X264Encoder.h new file mode 100644 index 0000000..62f57e9 --- /dev/null +++ b/linux/X264Encoder.h @@ -0,0 +1,471 @@ +#pragma once +/** + * X264Encoder.h - Linux H264 Encoder using libx264 + * + * Features: + * - Dynamic library loading (dlopen/dlsym) + * - Automatic fallback if libx264 not available + * - Manual BGRA→I420 conversion (no libyuv dependency) + * - API compatible with Windows X264Encoder + * + * Requirements: + * - libx264 installed (apt install libx264-dev) + * - If not installed, H264 encoding is disabled + */ + +#include +#include +#include +#include +#include + +// Include x264 header for struct definitions +// The library is dynamically loaded at runtime +extern "C" { +#include "../compress/x264/x264.h" +} + +// ============== X264Encoder Class ============== + +class X264Encoder { +public: + // Check if libx264 is available on this system + static bool IsAvailable() { + static int available = -1; + if (available < 0) { + void* handle = TryLoadLibrary(); + available = (handle != nullptr) ? 1 : 0; + if (handle) { + dlclose(handle); + fprintf(stderr, ">>> X264Encoder: libx264 available\n"); + } else { + fprintf(stderr, "*** X264Encoder: libx264 not found (%s)\n", dlerror()); + } + } + return available == 1; + } + + X264Encoder() + : m_x264Handle(nullptr) + , m_encoder(nullptr) + , m_picIn(nullptr) + , m_picOut(nullptr) + , m_width(0) + , m_height(0) + { + memset(&m_param, 0, sizeof(m_param)); + clearFunctionPointers(); + } + + ~X264Encoder() { + close(); + } + + bool open(int width, int height, int fps, int crf) { + close(); + + // Load library + if (!loadLibrary()) { + fprintf(stderr, "*** X264Encoder::open: loadLibrary failed\n"); + return false; + } + + // Round to even dimensions (H264 requirement) + m_width = width & ~1; + m_height = height & ~1; + + // Initialize parameters + if (fn_x264_param_default_preset) { + fn_x264_param_default_preset(&m_param, "ultrafast", "zerolatency"); + } + + // Set encoder parameters + m_param.i_width = m_width; + m_param.i_height = m_height; + m_param.i_log_level = X264_LOG_NONE; + m_param.i_threads = 1; + m_param.i_frame_total = 0; + m_param.i_keyint_max = fps * 15; // Keyframe every 15 seconds + m_param.i_bframe = 0; // No B-frames for low latency + m_param.b_open_gop = 0; + m_param.i_fps_num = fps; + m_param.i_fps_den = 1; + m_param.i_csp = X264_CSP_I420; + + // Rate control: CRF mode + m_param.rc.i_rc_method = X264_RC_CRF; + m_param.rc.f_rf_constant = (float)crf; + + // Apply baseline profile for compatibility + if (fn_x264_param_apply_profile) { + fn_x264_param_apply_profile(&m_param, "baseline"); + } + + // Allocate pictures + m_picIn = (x264_picture_t*)calloc(1, sizeof(x264_picture_t)); + m_picOut = (x264_picture_t*)calloc(1, sizeof(x264_picture_t)); + if (!m_picIn || !m_picOut) { + close(); + return false; + } + + // Initialize input picture + if (fn_x264_picture_init) { + fn_x264_picture_init(m_picIn); + } + + // Allocate picture buffer + if (fn_x264_picture_alloc) { + if (fn_x264_picture_alloc(m_picIn, X264_CSP_I420, m_width, m_height) < 0) { + close(); + return false; + } + } + + // Open encoder + m_encoder = fn_x264_encoder_open(&m_param); + if (!m_encoder) { + close(); + return false; + } + + return true; + } + + void close() { + if (m_encoder && fn_x264_encoder_close) { + fn_x264_encoder_close(m_encoder); + m_encoder = nullptr; + } + + if (m_picIn) { + if (fn_x264_picture_clean) { + fn_x264_picture_clean(m_picIn); + } + free(m_picIn); + m_picIn = nullptr; + } + + if (m_picOut) { + free(m_picOut); + m_picOut = nullptr; + } + + unloadLibrary(); + m_width = m_height = 0; + } + + /** + * Encode a frame + * @param bgra Input BGRA image data + * @param bpp Bits per pixel (24 or 32) + * @param stride Bytes per row + * @param width Image width + * @param height Image height + * @param outData Output: pointer to encoded H264 data + * @param outSize Output: size of encoded data + * @param direction 1 = normal, -1 = vertical flip + * @return 0 on success, negative on error + */ + int encode(uint8_t* bgra, uint8_t bpp, uint32_t stride, + uint32_t width, uint32_t height, + uint8_t** outData, uint32_t* outSize, + int direction = 1) + { + if (!m_encoder || !m_picIn || !fn_x264_encoder_encode) { + return -1; + } + + // Check dimensions match + if ((int)(width & ~1) != m_width || (int)(height & ~1) != m_height) { + return -2; + } + + // Convert BGRA to I420 directly into x264 picture planes + if (bpp == 32) { + convertBGRAtoI420(bgra, stride, direction, + m_picIn->img.plane[0], m_picIn->img.i_stride[0], + m_picIn->img.plane[1], m_picIn->img.i_stride[1], + m_picIn->img.plane[2], m_picIn->img.i_stride[2]); + } else if (bpp == 24) { + convertRGB24toI420(bgra, stride, direction, + m_picIn->img.plane[0], m_picIn->img.i_stride[0], + m_picIn->img.plane[1], m_picIn->img.i_stride[1], + m_picIn->img.plane[2], m_picIn->img.i_stride[2]); + } else { + return -3; + } + + // Encode + x264_nal_t* pNal = nullptr; + int iNal = 0; + int encodeSize = fn_x264_encoder_encode(m_encoder, &pNal, &iNal, m_picIn, m_picOut); + + if (encodeSize < 0) { + return -4; + } + + if (encodeSize == 0 || !pNal) { + *outData = nullptr; + *outSize = 0; + return 0; + } + + *outData = pNal->p_payload; + *outSize = encodeSize; + return 0; + } + +private: + // Library handle + void* m_x264Handle; + + // Encoder state + x264_t* m_encoder; + x264_param_t m_param; + x264_picture_t* m_picIn; + x264_picture_t* m_picOut; + int m_width, m_height; + + // x264 function pointers + void (*fn_x264_param_default_preset)(x264_param_t*, const char*, const char*); + int (*fn_x264_param_apply_profile)(x264_param_t*, const char*); + x264_t* (*fn_x264_encoder_open)(x264_param_t*); + void (*fn_x264_encoder_close)(x264_t*); + int (*fn_x264_encoder_encode)(x264_t*, x264_nal_t**, int*, x264_picture_t*, x264_picture_t*); + void (*fn_x264_picture_init)(x264_picture_t*); + int (*fn_x264_picture_alloc)(x264_picture_t*, int, int, int); + void (*fn_x264_picture_clean)(x264_picture_t*); + + void clearFunctionPointers() { + fn_x264_param_default_preset = nullptr; + fn_x264_param_apply_profile = nullptr; + fn_x264_encoder_open = nullptr; + fn_x264_encoder_close = nullptr; + fn_x264_encoder_encode = nullptr; + fn_x264_picture_init = nullptr; + fn_x264_picture_alloc = nullptr; + fn_x264_picture_clean = nullptr; + } + + static void* TryLoadLibrary() { + // Try multiple library versions (newest first) + const char* libNames[] = { + "libx264.so", // symlink (if exists) + "libx264.so.164", // Ubuntu 24, Debian 12+ + "libx264.so.163", + "libx264.so.162", + "libx264.so.161", + "libx264.so.160", + "libx264.so.159", + "libx264.so.157", + "libx264.so.155", // Ubuntu 20 + "libx264.so.152", + "libx264.so.148", // older distros + nullptr + }; + + for (int i = 0; libNames[i]; i++) { + void* handle = dlopen(libNames[i], RTLD_LAZY); + if (handle) return handle; + } + return nullptr; + } + + bool loadLibrary() { + m_x264Handle = TryLoadLibrary(); + if (!m_x264Handle) { + return false; + } + + // Load functions + fn_x264_param_default_preset = (decltype(fn_x264_param_default_preset)) + dlsym(m_x264Handle, "x264_param_default_preset"); + + fn_x264_param_apply_profile = (decltype(fn_x264_param_apply_profile)) + dlsym(m_x264Handle, "x264_param_apply_profile"); + + fn_x264_picture_init = (decltype(fn_x264_picture_init)) + dlsym(m_x264Handle, "x264_picture_init"); + + fn_x264_picture_alloc = (decltype(fn_x264_picture_alloc)) + dlsym(m_x264Handle, "x264_picture_alloc"); + + fn_x264_picture_clean = (decltype(fn_x264_picture_clean)) + dlsym(m_x264Handle, "x264_picture_clean"); + + fn_x264_encoder_close = (decltype(fn_x264_encoder_close)) + dlsym(m_x264Handle, "x264_encoder_close"); + + // x264_encoder_open has version suffix based on X264_BUILD + // Try common versions in order (newest first) + const char* openNames[] = { + "x264_encoder_open_164", + "x264_encoder_open_163", + "x264_encoder_open_162", + "x264_encoder_open_161", + "x264_encoder_open_160", + "x264_encoder_open_159", + "x264_encoder_open_157", + "x264_encoder_open_155", + "x264_encoder_open_152", + "x264_encoder_open_148", + nullptr + }; + + for (int i = 0; openNames[i]; i++) { + fn_x264_encoder_open = (decltype(fn_x264_encoder_open)) + dlsym(m_x264Handle, openNames[i]); + if (fn_x264_encoder_open) { + fprintf(stderr, ">>> X264Encoder: found %s\n", openNames[i]); + break; + } + } + + fn_x264_encoder_encode = (decltype(fn_x264_encoder_encode)) + dlsym(m_x264Handle, "x264_encoder_encode"); + + // Check required functions + if (!fn_x264_encoder_open || !fn_x264_encoder_encode || !fn_x264_encoder_close || + !fn_x264_param_default_preset || !fn_x264_picture_alloc) { + fprintf(stderr, "*** X264Encoder: missing functions - open=%p encode=%p close=%p preset=%p alloc=%p\n", + (void*)fn_x264_encoder_open, (void*)fn_x264_encoder_encode, + (void*)fn_x264_encoder_close, (void*)fn_x264_param_default_preset, + (void*)fn_x264_picture_alloc); + unloadLibrary(); + return false; + } + + return true; + } + + void unloadLibrary() { + if (m_x264Handle) { + dlclose(m_x264Handle); + m_x264Handle = nullptr; + } + clearFunctionPointers(); + } + + /** + * Convert BGRA to I420 (YUV 4:2:0 planar) directly into output planes + * Using ITU-R BT.601 coefficients + */ + void convertBGRAtoI420(const uint8_t* bgra, int stride, int direction, + uint8_t* yPlane, int yStride, + uint8_t* uPlane, int uStride, + uint8_t* vPlane, int vStride) { + int srcStride = stride; + int w = m_width; + int h = m_height; + + // Direction: 1 = normal, -1 = flip vertically + int startY = (direction > 0) ? 0 : (h - 1); + int stepY = (direction > 0) ? 1 : -1; + + // Y plane: full resolution + for (int j = 0; j < h; j++) { + int srcY = startY + j * stepY; + const uint8_t* srcRow = bgra + srcY * srcStride; + uint8_t* dstRow = yPlane + j * yStride; + + for (int i = 0; i < w; i++) { + uint8_t b = srcRow[i * 4 + 0]; + uint8_t g = srcRow[i * 4 + 1]; + uint8_t r = srcRow[i * 4 + 2]; + // Y = 0.257*R + 0.504*G + 0.098*B + 16 + int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16; + dstRow[i] = (uint8_t)(y < 0 ? 0 : (y > 255 ? 255 : y)); + } + } + + // U/V planes: half resolution (2x2 block averaging) + int uvW = w / 2; + int uvH = h / 2; + + for (int j = 0; j < uvH; j++) { + int srcY0 = startY + (j * 2) * stepY; + int srcY1 = startY + (j * 2 + 1) * stepY; + const uint8_t* row0 = bgra + srcY0 * srcStride; + const uint8_t* row1 = bgra + srcY1 * srcStride; + + for (int i = 0; i < uvW; i++) { + // Average 4 pixels + int r = 0, g = 0, b = 0; + + b += row0[(i*2+0)*4 + 0]; g += row0[(i*2+0)*4 + 1]; r += row0[(i*2+0)*4 + 2]; + b += row0[(i*2+1)*4 + 0]; g += row0[(i*2+1)*4 + 1]; r += row0[(i*2+1)*4 + 2]; + b += row1[(i*2+0)*4 + 0]; g += row1[(i*2+0)*4 + 1]; r += row1[(i*2+0)*4 + 2]; + b += row1[(i*2+1)*4 + 0]; g += row1[(i*2+1)*4 + 1]; r += row1[(i*2+1)*4 + 2]; + + r >>= 2; g >>= 2; b >>= 2; + + // U = -0.148*R - 0.291*G + 0.439*B + 128 + int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; + // V = 0.439*R - 0.368*G - 0.071*B + 128 + int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; + + uPlane[j * uStride + i] = (uint8_t)(u < 0 ? 0 : (u > 255 ? 255 : u)); + vPlane[j * vStride + i] = (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v)); + } + } + } + + /** + * Convert RGB24 to I420 (YUV 4:2:0 planar) directly into output planes + */ + void convertRGB24toI420(const uint8_t* rgb, int stride, int direction, + uint8_t* yPlane, int yStride, + uint8_t* uPlane, int uStride, + uint8_t* vPlane, int vStride) { + int srcStride = stride; + int w = m_width; + int h = m_height; + + int startY = (direction > 0) ? 0 : (h - 1); + int stepY = (direction > 0) ? 1 : -1; + + // Y plane + for (int j = 0; j < h; j++) { + int srcY = startY + j * stepY; + const uint8_t* srcRow = rgb + srcY * srcStride; + uint8_t* dstRow = yPlane + j * yStride; + + for (int i = 0; i < w; i++) { + uint8_t r = srcRow[i * 3 + 0]; + uint8_t g = srcRow[i * 3 + 1]; + uint8_t b = srcRow[i * 3 + 2]; + int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16; + dstRow[i] = (uint8_t)(y < 0 ? 0 : (y > 255 ? 255 : y)); + } + } + + // U/V planes + int uvW = w / 2; + int uvH = h / 2; + + for (int j = 0; j < uvH; j++) { + int srcY0 = startY + (j * 2) * stepY; + int srcY1 = startY + (j * 2 + 1) * stepY; + const uint8_t* row0 = rgb + srcY0 * srcStride; + const uint8_t* row1 = rgb + srcY1 * srcStride; + + for (int i = 0; i < uvW; i++) { + int r = 0, g = 0, b = 0; + + r += row0[(i*2+0)*3 + 0]; g += row0[(i*2+0)*3 + 1]; b += row0[(i*2+0)*3 + 2]; + r += row0[(i*2+1)*3 + 0]; g += row0[(i*2+1)*3 + 1]; b += row0[(i*2+1)*3 + 2]; + r += row1[(i*2+0)*3 + 0]; g += row1[(i*2+0)*3 + 1]; b += row1[(i*2+0)*3 + 2]; + r += row1[(i*2+1)*3 + 0]; g += row1[(i*2+1)*3 + 1]; b += row1[(i*2+1)*3 + 2]; + + r >>= 2; g >>= 2; b >>= 2; + + int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; + int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; + + uPlane[j * uStride + i] = (uint8_t)(u < 0 ? 0 : (u > 255 ? 255 : u)); + vPlane[j * vStride + i] = (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v)); + } + } + } +}; diff --git a/macos/ScreenHandler.h b/macos/ScreenHandler.h index 6118915..fd4d7c5 100644 --- a/macos/ScreenHandler.h +++ b/macos/ScreenHandler.h @@ -4,6 +4,7 @@ #import #import #import "../client/IOCPClient.h" +#import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_* #include #include #include @@ -33,11 +34,7 @@ struct BITMAPINFOHEADER_MAC { }; #pragma pack(pop) -// Screen algorithm constants -#define ALGORITHM_GRAY 0 -#define ALGORITHM_DIFF 1 -#define ALGORITHM_H264 2 -#define ALGORITHM_RGB565 3 +// Algorithm constants from commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565 class ScreenHandler : public IOCPManager { public: diff --git a/macos/ScreenHandler.mm b/macos/ScreenHandler.mm index a5cf032..6ba67a5 100644 --- a/macos/ScreenHandler.mm +++ b/macos/ScreenHandler.mm @@ -357,28 +357,40 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist) { m_qualityLevel = level; + // TODO: persist to config file if needed + (void)persist; + if (level == QUALITY_DISABLED) { - NSLog(@"Quality: Disabled"); + // Disabled mode: keep current settings + NSLog(@"Quality: Disabled (keep current)"); 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]); + // Get profile from commands.h (shared with Windows/Linux) + const QualityProfile& profile = GetQualityProfile(level); + + // Apply FPS + m_maxFPS.store(profile.maxFPS); + + // Apply algorithm (macOS supports all algorithms including H264 via VideoToolbox) + m_algorithm.store(profile.algorithm); + + // Update H264 bitrate if applicable + if (profile.algorithm == ALGORITHM_H264 && profile.bitRate > 0) { + m_h264Bitrate = profile.bitRate * 1000; // kbps -> bps + } + + NSLog(@"Quality: Level=%d (%s), FPS=%d, Algo=%d, BitRate=%d kbps", + level, + level == QUALITY_ULTRA ? "Ultra" : + level == QUALITY_HIGH ? "High" : + level == QUALITY_GOOD ? "Good" : + level == QUALITY_MEDIUM ? "Medium" : + level == QUALITY_LOW ? "Low" : "Minimal", + profile.maxFPS, profile.algorithm, profile.bitRate); } else { + // Adaptive mode (level=-1): server adjusts dynamically NSLog(@"Quality: Adaptive mode"); } }