Feat: Implement H264 for Linux client with dynamic libx264 loading

This commit is contained in:
yuanyuanxiang
2026-05-03 12:15:22 +02:00
parent 36423b1c7c
commit ca37fa419a
6 changed files with 646 additions and 38 deletions

View File

@@ -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) {

View File

@@ -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 = 01080p 限制
- strategy = 1原始分辨率

View File

@@ -4,6 +4,7 @@
#include "LinuxConfig.h"
#include "ClipboardHandler.h"
#include "common/FileTransferV2.h"
#include "X264Encoder.h"
#include <dlfcn.h>
#include <sys/stat.h>
#include <thread>
@@ -12,6 +13,7 @@
#include <vector>
#include <cstring>
#include <stdexcept>
#include <memory>
// 客户端 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<uint8_t> m_diffBuffer;
// 自适应质量控制
std::atomic<uint8_t> m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY)
std::atomic<uint8_t> m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY/H264)
std::atomic<int> m_maxFPS; // 最大帧率
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
// H264 编码器
std::unique_ptr<X264Encoder> 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<uint8_t> 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());

471
linux/X264Encoder.h Normal file
View File

@@ -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 <stdint.h>
#include <cstring>
#include <cstdlib>
#include <dlfcn.h>
#include <vector>
// 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));
}
}
}
};

View File

@@ -4,6 +4,7 @@
#import <dispatch/dispatch.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
#import "../client/IOCPClient.h"
#import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_*
#include <vector>
#include <atomic>
#include <mutex>
@@ -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:

View File

@@ -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");
}
}