Feat: Implement H264 for Linux client with dynamic libx264 loading
This commit is contained in:
@@ -202,6 +202,8 @@ private:
|
|||||||
// macOS locale settings
|
// macOS locale settings
|
||||||
setenv("LANG", "en_US.UTF-8", 1);
|
setenv("LANG", "en_US.UTF-8", 1);
|
||||||
setenv("LC_ALL", "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
|
// Try zsh first (macOS default), fallback to bash
|
||||||
if (access("/bin/zsh", X_OK) == 0) {
|
if (access("/bin/zsh", X_OK) == 0) {
|
||||||
|
|||||||
@@ -1048,6 +1048,14 @@ enum QualityLevel {
|
|||||||
QUALITY_COUNT = 6,
|
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 对应)
|
/* 质量配置(与 QualityLevel 对应)
|
||||||
- strategy = 0:1080p 限制
|
- strategy = 0:1080p 限制
|
||||||
- strategy = 1:原始分辨率
|
- strategy = 1:原始分辨率
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "LinuxConfig.h"
|
#include "LinuxConfig.h"
|
||||||
#include "ClipboardHandler.h"
|
#include "ClipboardHandler.h"
|
||||||
#include "common/FileTransferV2.h"
|
#include "common/FileTransferV2.h"
|
||||||
|
#include "X264Encoder.h"
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
// 客户端 ID(定义在 main.cpp)
|
// 客户端 ID(定义在 main.cpp)
|
||||||
extern uint64_t g_myClientID;
|
extern uint64_t g_myClientID;
|
||||||
@@ -110,27 +112,39 @@ struct XGCValues_LNX {
|
|||||||
#define IncludeInferiors 1
|
#define IncludeInferiors 1
|
||||||
|
|
||||||
// ============== 屏幕算法常量 ==============
|
// ============== 屏幕算法常量 ==============
|
||||||
#define ALGORITHM_GRAY 0
|
// 常量定义已移至 commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
|
||||||
#define ALGORITHM_DIFF 1
|
|
||||||
#define ALGORITHM_H264 2
|
|
||||||
#define ALGORITHM_RGB565 3
|
|
||||||
|
|
||||||
// 算法支持表(编译时常量,日后支持 H264 时改为 true)
|
// 检查算法是否支持(H264 需要运行时检测)
|
||||||
static const bool g_SupportedAlgo[] = {
|
inline bool IsAlgorithmSupported(uint8_t algo) {
|
||||||
true, // ALGORITHM_GRAY = 0
|
switch (algo) {
|
||||||
true, // ALGORITHM_DIFF = 1
|
case ALGORITHM_GRAY:
|
||||||
false, // ALGORITHM_H264 = 2
|
case ALGORITHM_DIFF:
|
||||||
true, // ALGORITHM_RGB565 = 3
|
case ALGORITHM_RGB565:
|
||||||
};
|
return true;
|
||||||
|
case ALGORITHM_H264:
|
||||||
|
return X264Encoder::IsAvailable();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 不支持的算法降级为 RGB565
|
// 不支持的算法降级为 RGB565
|
||||||
inline uint8_t GetEffectiveAlgorithm(uint8_t algo) {
|
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 ALGORITHM_RGB565;
|
||||||
}
|
}
|
||||||
return algo;
|
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)
|
// BGRA → 灰度 (Y = 0.299R + 0.587G + 0.114B)
|
||||||
@@ -538,7 +552,8 @@ public:
|
|||||||
m_inputDisplay(nullptr),
|
m_inputDisplay(nullptr),
|
||||||
m_width(0), m_height(0),
|
m_width(0), m_height(0),
|
||||||
m_pixmap(0), m_gc(nullptr), m_xtestWarned(false),
|
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) {
|
if (!client) {
|
||||||
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
||||||
@@ -883,12 +898,28 @@ public:
|
|||||||
// 应用帧率
|
// 应用帧率
|
||||||
m_maxFPS.store(profile.maxFPS);
|
m_maxFPS.store(profile.maxFPS);
|
||||||
|
|
||||||
|
// 应用码率(H264 使用)
|
||||||
|
int oldBitrate = m_h264Bitrate;
|
||||||
|
m_h264Bitrate = profile.bitRate;
|
||||||
|
|
||||||
// 应用算法(带降级处理)
|
// 应用算法(带降级处理)
|
||||||
uint8_t algo = GetEffectiveAlgorithm(profile.algorithm);
|
uint8_t algo = GetEffectiveAlgorithm(profile.algorithm);
|
||||||
|
uint8_t oldAlgo = m_bAlgorithm.load();
|
||||||
m_bAlgorithm.store(algo);
|
m_bAlgorithm.store(algo);
|
||||||
|
|
||||||
Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d\n",
|
// 如果 H264 参数变化,需要重新初始化编码器
|
||||||
level, profile.maxFPS, profile.algorithm, algo);
|
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 {
|
} else {
|
||||||
// 自适应模式 (level=-1):由服务端动态调整,不做处理
|
// 自适应模式 (level=-1):由服务端动态调整,不做处理
|
||||||
Mprintf(">>> Quality: Adaptive mode\n");
|
Mprintf(">>> Quality: Adaptive mode\n");
|
||||||
@@ -1054,11 +1085,15 @@ private:
|
|||||||
std::vector<uint8_t> m_diffBuffer;
|
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; // 最大帧率
|
std::atomic<int> m_maxFPS; // 最大帧率
|
||||||
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
|
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
|
||||||
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
|
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
|
||||||
|
|
||||||
|
// H264 编码器
|
||||||
|
std::unique_ptr<X264Encoder> m_h264Encoder;
|
||||||
|
int m_h264Bitrate; // 码率 (kbps)
|
||||||
|
|
||||||
// X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致)
|
// X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致)
|
||||||
// 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap,再对 Pixmap 调用 XGetImage
|
// 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap,再对 Pixmap 调用 XGetImage
|
||||||
// 这样可以避免合成窗口管理器(Mutter 等)导致的 BadMatch 错误
|
// 这样可以避免合成窗口管理器(Mutter 等)导致的 BadMatch 错误
|
||||||
@@ -1151,6 +1186,59 @@ private:
|
|||||||
std::swap(m_prevFrame, m_currFrame);
|
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)
|
// 差异比较算法(支持 DIFF/RGB565/GRAY)
|
||||||
// 输出格式: [byteOffset(4) + length(4) + pixel data] ...
|
// 输出格式: [byteOffset(4) + length(4) + pixel data] ...
|
||||||
// DIFF: length = 字节数, data = BGRA 原始数据
|
// DIFF: length = 字节数, data = BGRA 原始数据
|
||||||
@@ -1243,10 +1331,34 @@ private:
|
|||||||
// 发送第一帧
|
// 发送第一帧
|
||||||
SendFirstScreen();
|
SendFirstScreen();
|
||||||
|
|
||||||
|
uint8_t currentAlgo = m_bAlgorithm.load();
|
||||||
|
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
uint64_t start = GetTickMs();
|
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)
|
// 动态计算帧间隔(根据当前 maxFPS)
|
||||||
int fps = m_maxFPS.load();
|
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");
|
Mprintf(">>> ScreenHandler CaptureLoop stopped\n");
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
Mprintf("*** CaptureLoop exception: %s ***\n", e.what());
|
Mprintf("*** CaptureLoop exception: %s ***\n", e.what());
|
||||||
|
|||||||
471
linux/X264Encoder.h
Normal file
471
linux/X264Encoder.h
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#import <dispatch/dispatch.h>
|
#import <dispatch/dispatch.h>
|
||||||
#import <IOKit/pwr_mgt/IOPMLib.h>
|
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||||
#import "../client/IOCPClient.h"
|
#import "../client/IOCPClient.h"
|
||||||
|
#import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_*
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -33,11 +34,7 @@ struct BITMAPINFOHEADER_MAC {
|
|||||||
};
|
};
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
// Screen algorithm constants
|
// Algorithm constants from commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
|
||||||
#define ALGORITHM_GRAY 0
|
|
||||||
#define ALGORITHM_DIFF 1
|
|
||||||
#define ALGORITHM_H264 2
|
|
||||||
#define ALGORITHM_RGB565 3
|
|
||||||
|
|
||||||
class ScreenHandler : public IOCPManager {
|
class ScreenHandler : public IOCPManager {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -357,28 +357,40 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
|
|||||||
{
|
{
|
||||||
m_qualityLevel = level;
|
m_qualityLevel = level;
|
||||||
|
|
||||||
|
// TODO: persist to config file if needed
|
||||||
|
(void)persist;
|
||||||
|
|
||||||
if (level == QUALITY_DISABLED) {
|
if (level == QUALITY_DISABLED) {
|
||||||
NSLog(@"Quality: Disabled");
|
// Disabled mode: keep current settings
|
||||||
|
NSLog(@"Quality: Disabled (keep current)");
|
||||||
return;
|
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) {
|
if (level >= 0 && level < QUALITY_COUNT) {
|
||||||
m_maxFPS.store(profiles[level][0]);
|
// Get profile from commands.h (shared with Windows/Linux)
|
||||||
m_algorithm.store(profiles[level][1]);
|
const QualityProfile& profile = GetQualityProfile(level);
|
||||||
NSLog(@"Quality: Level=%d, FPS=%d, Algo=%d", level, profiles[level][0], profiles[level][1]);
|
|
||||||
|
// 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 {
|
} else {
|
||||||
|
// Adaptive mode (level=-1): server adjusts dynamically
|
||||||
NSLog(@"Quality: Adaptive mode");
|
NSLog(@"Quality: Adaptive mode");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user