diff --git a/.gitignore b/.gitignore index e3afe81..8f87dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ Releases/* linux/Makefile linux/cmake_install.cmake .vs +client/ghost_vs2015.vcxproj.user docs/macOS_Support_Design.md settings.local.json *.zip diff --git a/client/CFFmpegAV1Encoder.cpp b/client/CFFmpegAV1Encoder.cpp new file mode 100644 index 0000000..80368c8 --- /dev/null +++ b/client/CFFmpegAV1Encoder.cpp @@ -0,0 +1,243 @@ +#include "CFFmpegAV1Encoder.h" +#include "common/config.h" +#include "common/logger.h" + +// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整个实现移出编译单元(FFmpeg lib 已在 +// CFFmpegH264Encoder.cpp 用同条件链接,此处不重复 #pragma comment) +#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST + +extern "C" { +#include +#include +#include +#include +} + +#include + +// FFmpeg / 系统库已经由 CFFmpegH264Encoder.cpp 的 #pragma comment(lib) 引入。 +// 这里不再重复声明(重复 #pragma comment 在同一 link 单元不冲突但冗余)。 + +// av_opt_set 包装:拼错的参数值会被 FFmpeg 静默忽略,包一层日志便于发现。 +// 实现与 CFFmpegH264Encoder 内的 helper 相同;放成 static 文件内可见即可。 +static void setOpt(void* obj, const char* name, const char* val, const char* backend) { + int rc = av_opt_set(obj, name, val, 0); + if (rc < 0) { + char errbuf[128] = {0}; + av_strerror(rc, errbuf, sizeof(errbuf)); + Mprintf("[WARN] av_opt_set('%s'='%s') on %s failed (%d): %s\n", + name, val, backend, rc, errbuf); + } +} +static void setOptInt(void* obj, const char* name, int64_t val, const char* backend) { + int rc = av_opt_set_int(obj, name, val, 0); + if (rc < 0) { + char errbuf[128] = {0}; + av_strerror(rc, errbuf, sizeof(errbuf)); + Mprintf("[WARN] av_opt_set_int('%s'=%lld) on %s failed (%d): %s\n", + name, (long long)val, backend, rc, errbuf); + } +} + +// AV1 硬编后端探测顺序,没有 av1_mf 兜底(FFmpeg 7.1 不支持)。 +// 全失败时 EncoderFactory 自动回退到 H.264 路径,行为对称。 +static const char* kAV1Backends[] = { + "av1_nvenc", // NVIDIA RTX 40 / 50 系(Ada Lovelace+) + "av1_amf", // AMD RX 7000+(RDNA 3+) + "av1_qsv", // Intel Arc 独显 / 部分 11 代+ 核显 +}; + +CFFmpegAV1Encoder::CFFmpegAV1Encoder() = default; + +CFFmpegAV1Encoder::~CFFmpegAV1Encoder() { + close(); +} + +void CFFmpegAV1Encoder::cleanupCodec() { + if (m_packet) { av_packet_free(&m_packet); m_packet = nullptr; } + if (m_frame) { av_frame_free(&m_frame); m_frame = nullptr; } + if (m_ctx) { avcodec_free_context(&m_ctx); m_ctx = nullptr; } +} + +void CFFmpegAV1Encoder::close() { + cleanupCodec(); + m_backend.clear(); + m_pts = 0; + m_forceIDR = false; +} + +bool CFFmpegAV1Encoder::open(const EncoderParams& params) { + close(); + for (const char* name : kAV1Backends) { + if (tryOpenBackend(name, params)) { + m_backend = name; + return true; + } + cleanupCodec(); + } + return false; +} + +bool CFFmpegAV1Encoder::tryOpenBackend(const char* name, const EncoderParams& p) { + const AVCodec* codec = avcodec_find_encoder_by_name(name); + if (!codec) { + // AV1 硬编没注册 = 老 ffmpeg lib 不含 AV1 encoder(compress\ffmpeg 没启用 av1) + Mprintf("=> FFmpeg: AV1 encoder '%s' NOT in linked lib\n", name); + return false; + } + + m_ctx = avcodec_alloc_context3(codec); + if (!m_ctx) { + Mprintf("=> FFmpeg: avcodec_alloc_context3('%s') failed\n", name); + return false; + } + + m_ctx->width = p.width & ~1; + m_ctx->height = p.height & ~1; + m_ctx->time_base = AVRational{1, p.fps}; + m_ctx->framerate = AVRational{p.fps, 1}; + m_ctx->pix_fmt = AV_PIX_FMT_NV12; + m_ctx->gop_size = p.fps * (p.gop_seconds > 0 ? p.gop_seconds : 15); + m_ctx->max_b_frames = 0; + m_ctx->bit_rate = (int64_t)p.bitrate_kbps * 1000; + m_ctx->rc_max_rate = (int64_t)p.bitrate_kbps * 1500; + m_ctx->rc_buffer_size = (int)(p.bitrate_kbps * 1000); + + // RC 策略与 H.264 路径对齐:peak-constrained VBR,远控静态画面省带宽。 + if (strcmp(name, "av1_nvenc") == 0) { + // av1_nvenc preset p1~p7;远控 p5 兼顾质量与速度。 + // tile-columns=1 把帧切两列,解码端并行更友好(浏览器 AV1 解码常用 SIMD/多线程) + setOpt(m_ctx->priv_data, "preset", "p5", name); + setOpt(m_ctx->priv_data, "tune", "ll", name); + setOpt(m_ctx->priv_data, "rc", "vbr", name); + setOpt(m_ctx->priv_data, "zerolatency", "1", name); + setOptInt(m_ctx->priv_data, "tile-columns", 1, name); + } else if (strcmp(name, "av1_amf") == 0) { + // av1_amf 选项命名与 h264_amf 大体一致,rc 同样支持 vbr_peak + // (见 ffmpeg -h encoder=av1_amf)。静态画面省码率四件套同 H.264 路径。 + setOpt(m_ctx->priv_data, "usage", "lowlatency", name); + setOpt(m_ctx->priv_data, "quality", "quality", name); + setOpt(m_ctx->priv_data, "rc", "vbr_peak", name); + setOptInt(m_ctx->priv_data, "vbaq", 1, name); + setOptInt(m_ctx->priv_data, "preanalysis", 1, name); + setOptInt(m_ctx->priv_data, "filler_data", 0, name); + setOptInt(m_ctx->priv_data, "enforce_hrd", 0, name); + } else if (strcmp(name, "av1_qsv") == 0) { + // av1_qsv:bit_rate < max_rate 时自动 VBR + setOpt(m_ctx->priv_data, "preset", "slow", name); + setOptInt(m_ctx->priv_data, "async_depth", 1, name); + setOptInt(m_ctx->priv_data, "low_power", 0, name); + } + + int ret = avcodec_open2(m_ctx, codec, nullptr); + if (ret < 0) { + // 找到了但开不起来:无对应 GPU / 驱动太旧 / 跨适配器 + char errbuf[128] = {0}; + av_strerror(ret, errbuf, sizeof(errbuf)); + Mprintf("=> FFmpeg: avcodec_open2('%s') failed (%d): %s\n", name, ret, errbuf); + return false; + } + + m_frame = av_frame_alloc(); + if (!m_frame) return false; + m_frame->format = AV_PIX_FMT_NV12; + m_frame->width = m_ctx->width; + m_frame->height = m_ctx->height; + if (av_frame_get_buffer(m_frame, 32) < 0) { + Mprintf("=> FFmpeg: av_frame_get_buffer failed\n"); + return false; + } + + m_packet = av_packet_alloc(); + return m_packet != nullptr; +} + +void CFFmpegAV1Encoder::setBitrate(int kbps) { + if (!m_ctx) return; + m_ctx->bit_rate = (int64_t)kbps * 1000; + m_ctx->rc_max_rate = (int64_t)kbps * 1500; + m_ctx->rc_buffer_size = (int)(kbps * 1000); + // 同 H.264 路径:多数硬编不支持运行时改 bit_rate 让 ctx 立刻生效; + // 这里仅更新数值,下次 open 时生效。 +} + +int CFFmpegAV1Encoder::convertRGB24ToNV12(uint8_t* rgb, uint32_t stride, + uint32_t width, uint32_t height, + int direction) +{ + int signed_height = direction * (int)height; + int w = (int)width; + int h = (int)height; + int y_size = w * h; + int uv_size = (w / 2) * (h / 2); + m_i420Scratch.resize(y_size + 2 * uv_size); + + uint8_t* y = m_i420Scratch.data(); + uint8_t* u = y + y_size; + uint8_t* v = u + uv_size; + + if (libyuv::RGB24ToI420(rgb, stride, y, w, u, w / 2, v, w / 2, w, signed_height) != 0) + return -1; + if (libyuv::I420ToNV12(y, w, u, w / 2, v, w / 2, + m_frame->data[0], m_frame->linesize[0], + m_frame->data[1], m_frame->linesize[1], + w, h) != 0) + return -1; + return 0; +} + +int CFFmpegAV1Encoder::encode( + uint8_t* rgb, uint8_t bpp, uint32_t stride, + uint32_t width, uint32_t height, + uint8_t** lppData, uint32_t* lpSize, int direction) +{ + if (!m_ctx || !m_frame || !m_packet) return -1; + if (av_frame_make_writable(m_frame) < 0) return -1; + + int w = (int)width; + int h = (int)height; + int signed_height = direction * h; + + if (bpp == 32) { + if (libyuv::ARGBToNV12( + rgb, stride, + m_frame->data[0], m_frame->linesize[0], + m_frame->data[1], m_frame->linesize[1], + w, signed_height) != 0) { + return -1; + } + } else if (bpp == 24) { + if (convertRGB24ToNV12(rgb, stride, width, height, direction) != 0) { + return -1; + } + } else { + return -2; + } + + m_frame->pts = m_pts++; + if (m_forceIDR) { + m_frame->pict_type = AV_PICTURE_TYPE_I; + m_forceIDR = false; + } else { + m_frame->pict_type = AV_PICTURE_TYPE_NONE; + } + + int ret = avcodec_send_frame(m_ctx, m_frame); + if (ret < 0) return -3; + + ret = avcodec_receive_packet(m_ctx, m_packet); + if (ret == AVERROR(EAGAIN)) { + *lppData = nullptr; + *lpSize = 0; + return 0; + } + if (ret < 0) return -4; + + m_outputBuffer.assign(m_packet->data, m_packet->data + m_packet->size); + *lppData = m_outputBuffer.data(); + *lpSize = (uint32_t)m_outputBuffer.size(); + av_packet_unref(m_packet); + return 0; +} + +#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST diff --git a/client/CFFmpegAV1Encoder.h b/client/CFFmpegAV1Encoder.h new file mode 100644 index 0000000..5035486 --- /dev/null +++ b/client/CFFmpegAV1Encoder.h @@ -0,0 +1,62 @@ +#pragma once + +#include "VideoEncoderBase.h" +#include "common/config.h" +#include +#include + +// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整类移出编译单元,避免 GPL 传染(与 c0a632a 对齐) +#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST + +struct AVCodecContext; +struct AVFrame; +struct AVPacket; + +// FFmpeg 硬编 AV1 实现。 +// 后端探测顺序:av1_nvenc (NVIDIA RTX 40+) → av1_amf (AMD RX 7000+) → av1_qsv +// (Intel Arc / 11 代+ 部分核显)。AV1 硬编硬件门槛比 H.264 高得多 —— 没合适 +// 硬件时 open 全部失败,由 EncoderFactory 自动回退到 H.264 路径。 +// +// 注意:FFmpeg 7.1 没有 av1_mf 兜底,因此本类的探测列表比 H.264 短一项。 +class CFFmpegAV1Encoder : public VideoEncoderBase +{ +public: + CFFmpegAV1Encoder(); + ~CFFmpegAV1Encoder() override; + + bool open(const EncoderParams& params) override; + void close() override; + + int encode( + uint8_t* rgb, + uint8_t bpp, + uint32_t stride, + uint32_t width, + uint32_t height, + uint8_t** lppData, + uint32_t* lpSize, + int direction = 1 + ) override; + + void forceIDR() override { m_forceIDR = true; } + void setBitrate(int kbps) override; + VideoCodec codec() const override { return VideoCodec::AV1; } + const char* backendName() const override { return m_backend.c_str(); } + +private: + bool tryOpenBackend(const char* name, const EncoderParams& p); + void cleanupCodec(); + int convertRGB24ToNV12(uint8_t* rgb, uint32_t stride, + uint32_t width, uint32_t height, int direction); + + AVCodecContext* m_ctx = nullptr; + AVFrame* m_frame = nullptr; + AVPacket* m_packet = nullptr; + std::vector m_outputBuffer; + std::vector m_i420Scratch; + int64_t m_pts = 0; + bool m_forceIDR = false; + std::string m_backend; +}; + +#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST diff --git a/client/CFFmpegH264Encoder.cpp b/client/CFFmpegH264Encoder.cpp new file mode 100644 index 0000000..e802d76 --- /dev/null +++ b/client/CFFmpegH264Encoder.cpp @@ -0,0 +1,299 @@ +#include "CFFmpegH264Encoder.h" +#include "common/config.h" +#include "common/logger.h" + +// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整个实现 + 所有 #pragma comment(lib,"ffmpeg/...") +// 都不进编译单元,FFmpeg 静态库不会被链接进二进制 +#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST + +extern "C" { +#include +#include +#include +#include +} + +#include +#include + +// FFmpeg 静态库 + 必要的 Windows 系统库。x86 build 不引入,由 _WIN64 守护。 +// FFmpeg 三个核心库是纯 C,CRT 中性,Debug/Release 共用一份。 +#pragma comment(lib,"ffmpeg/libavcodec_x64.lib") +#pragma comment(lib,"ffmpeg/libavutil_x64.lib") +#pragma comment(lib,"ffmpeg/libswresample_x64.lib") +// dav1d (AV1 软解,C 项目) —— 不分 Debug/Release。 +// build 时启用了 --enable-libdav1d,libavcodec 内部 av1 decoder 引用了 dav1d 符号。 +#pragma comment(lib,"ffmpeg/dav1d_x64.lib") +// libvpl (Intel QSV, C++ 项目) —— 强制 CRT 一致,必须按 _DEBUG 切。 +// build 时启用了 --enable-libvpl,libavcodec 内部 h264_qsv / av1_qsv encoder 引用 MFX 符号。 +#ifdef _DEBUG +#pragma comment(lib,"ffmpeg/vpl_x64d.lib") +#else +#pragma comment(lib,"ffmpeg/vpl_x64.lib") +#endif + +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfuuid.lib") +#pragma comment(lib, "strmiids.lib") +#pragma comment(lib, "secur32.lib") +#pragma comment(lib, "bcrypt.lib") +#pragma comment(lib, "advapi32.lib") +#pragma comment(lib, "ole32.lib") +// ws2_32 在 IOCPClient.h 已 link,重复不冲突 +#pragma comment(lib, "ws2_32.lib") + +// av_opt_set wrappers:FFmpeg 在选项名/值拼错时 silently 返回 AVERROR_OPTION_NOT_FOUND +// 不报错,导致 encoder 退回默认行为且没人察觉(实际踩过:AMF rc=vbr_peak_constrained +// 拼成全名,FFmpeg 实际只接受 vbr_peak,没设上去就退回 CBR)。 +// 包一层 helper,任何设置失败 Mprintf 警告。 +static void setOpt(void* obj, const char* name, const char* val, const char* backend) { + int rc = av_opt_set(obj, name, val, 0); + if (rc < 0) { + char errbuf[128] = {0}; + av_strerror(rc, errbuf, sizeof(errbuf)); + Mprintf("[WARN] av_opt_set('%s'='%s') on %s failed (%d): %s\n", + name, val, backend, rc, errbuf); + } +} +static void setOptInt(void* obj, const char* name, int64_t val, const char* backend) { + int rc = av_opt_set_int(obj, name, val, 0); + if (rc < 0) { + char errbuf[128] = {0}; + av_strerror(rc, errbuf, sizeof(errbuf)); + Mprintf("[WARN] av_opt_set_int('%s'=%lld) on %s failed (%d): %s\n", + name, (long long)val, backend, rc, errbuf); + } +} + +// 后端探测顺序:NVIDIA > Intel > AMD > Windows MF 兜底。 +// open() 主循环按顺序试,第一个 avcodec_open2 成功的就用。 +// h264_mf 质量/稳定性一般,但是 Windows 系统级 hwaccel,任何 GPU 都能尝试,作最后兜底。 +static const char* kH264Backends[] = { + "h264_nvenc", // NVIDIA NVENC + "h264_qsv", // Intel Quick Sync Video + "h264_amf", // AMD AMF + "h264_mf", // Windows Media Foundation +}; + +CFFmpegH264Encoder::CFFmpegH264Encoder() = default; + +CFFmpegH264Encoder::~CFFmpegH264Encoder() { + close(); +} + +void CFFmpegH264Encoder::cleanupCodec() { + if (m_packet) { av_packet_free(&m_packet); m_packet = nullptr; } + if (m_frame) { av_frame_free(&m_frame); m_frame = nullptr; } + if (m_ctx) { avcodec_free_context(&m_ctx); m_ctx = nullptr; } +} + +void CFFmpegH264Encoder::close() { + cleanupCodec(); + m_backend.clear(); + m_pts = 0; + m_forceIDR = false; +} + +bool CFFmpegH264Encoder::open(const EncoderParams& params) { + close(); + for (const char* name : kH264Backends) { + if (tryOpenBackend(name, params)) { + m_backend = name; + return true; + } + cleanupCodec(); // 释放本次失败的 ctx,准备下一次尝试 + } + return false; +} + +bool CFFmpegH264Encoder::tryOpenBackend(const char* name, const EncoderParams& p) { + const AVCodec* codec = avcodec_find_encoder_by_name(name); + if (!codec) { + // 失败 = lib 里没注册这个 encoder。几乎肯定是链到了老 ffmpeg lib。 + Mprintf("=> FFmpeg: encoder '%s' NOT in linked lib (old ffmpeg?)\n", name); + return false; + } + + m_ctx = avcodec_alloc_context3(codec); + if (!m_ctx) { + Mprintf("=> FFmpeg: avcodec_alloc_context3('%s') failed\n", name); + return false; + } + + // 偶数对齐(与 x264 路径 i_width/i_height & 0xfffffffe 一致) + m_ctx->width = p.width & ~1; + m_ctx->height = p.height & ~1; + m_ctx->time_base = AVRational{1, p.fps}; + m_ctx->framerate = AVRational{p.fps, 1}; + m_ctx->pix_fmt = AV_PIX_FMT_NV12; + m_ctx->gop_size = p.fps * (p.gop_seconds > 0 ? p.gop_seconds : 4); + m_ctx->max_b_frames = 0; + m_ctx->bit_rate = (int64_t)p.bitrate_kbps * 1000; + m_ctx->rc_max_rate = (int64_t)p.bitrate_kbps * 1500; + m_ctx->rc_buffer_size = (int)(p.bitrate_kbps * 1000); + + // RC 策略选择:远程办公 90% 时间是静态画面(文档/IDE/邮件),CBR 会强行 + // 把目标码率填满(静态用不上的部分浪费带宽)。所有硬编后端统一改用 VBR, + // bit_rate 是平均目标、rc_max_rate (1.5x) 是峰值上限:静态时 encoder 自动 + // 降码率省带宽,动态时回到目标 + 短暂上探到 1.5x 保证画质。 + // 接近 x264 软编 CRF + VBV 的行为,但严格守住峰值不爆。 + if (strcmp(name, "h264_nvenc") == 0) { + // NVENC preset: p1(最快/低质) ~ p7(最慢/高质),远控低延迟 p5 兼顾。 + // tune=ll low-latency;rc=vbr 配 max_rate 实现峰值受限的 VBR。 + setOpt(m_ctx->priv_data, "preset", "p5", name); + setOpt(m_ctx->priv_data, "tune", "ll", name); + setOpt(m_ctx->priv_data, "rc", "vbr", name); + setOpt(m_ctx->priv_data, "zerolatency", "1", name); + } else if (strcmp(name, "h264_qsv") == 0) { + // Intel Quick Sync Video。preset: veryfast/faster/fast/medium/slow/slower/veryslow + // QSV 当 bit_rate != rc_max_rate 时自动走 VBR,所以这里只需调 preset。 + // preset=slow 比 medium 慢但画质好,async_depth=1 单帧立即出包。 + // low_power=0 走 PAK 路径,部分集显不支持 low_power 模式。 + setOpt(m_ctx->priv_data, "preset", "slow", name); + setOptInt(m_ctx->priv_data, "async_depth", 1, name); + setOptInt(m_ctx->priv_data, "low_power", 0, name); + } else if (strcmp(name, "h264_amf") == 0) { + // AMD AMF 远控低延迟配置: + // usage=ultralowlatency 比 lowlatency 更激进,关闭一切 lookahead; + // quality=speed 选最快编码路径(vs balanced/quality); + // rc=cbr 提供最可预测的输出节拍,避免 RC 切换抖动。 + // 静态画面省码率交给应用层 skip 检测(ScreenCapture::GetNextScreenData + // 已经过 memcmp 把无变化帧直接拦在编码器之前),不再依赖 vbaq/preanalysis + // 这些会引入 30-100ms lookahead 的"省码率三件套"。 + setOpt(m_ctx->priv_data, "usage", "ultralowlatency", name); + setOpt(m_ctx->priv_data, "quality", "speed", name); + setOpt(m_ctx->priv_data, "rc", "cbr", name); + setOptInt(m_ctx->priv_data, "filler_data", 0, name); + setOptInt(m_ctx->priv_data, "enforce_hrd", 0, name); + } else if (strcmp(name, "h264_mf") == 0) { + // Windows Media Foundation 兜底。rate_control 实际值(ffmpeg -h encoder=h264_mf): + // default / cbr / pc_vbr / u_vbr / quality / ld_vbr / g_vbr / gld_vbr + // 远控用 pc_vbr (peak-constrained VBR) 与其他后端语义对齐。 + setOptInt(m_ctx->priv_data, "hw_encoding", 1, name); + setOpt(m_ctx->priv_data, "rate_control", "pc_vbr", name); + } + + int ret = avcodec_open2(m_ctx, codec, nullptr); + if (ret < 0) { + // 失败 = encoder 找到了但开不起来。常见:无 NVIDIA GPU / 驱动太旧 / + // NVENC session 占满 / 笔记本独显未唤醒 / 参数组合驱动不接受 + char errbuf[128] = {0}; + av_strerror(ret, errbuf, sizeof(errbuf)); + Mprintf("=> FFmpeg: avcodec_open2('%s') failed (%d): %s\n", name, ret, errbuf); + return false; + } + + m_frame = av_frame_alloc(); + if (!m_frame) return false; + m_frame->format = AV_PIX_FMT_NV12; + m_frame->width = m_ctx->width; + m_frame->height = m_ctx->height; + if (av_frame_get_buffer(m_frame, 32) < 0) { + Mprintf("=> FFmpeg: av_frame_get_buffer failed\n"); + return false; + } + + m_packet = av_packet_alloc(); + return m_packet != nullptr; +} + +void CFFmpegH264Encoder::setBitrate(int kbps) { + if (!m_ctx) return; + m_ctx->bit_rate = (int64_t)kbps * 1000; + m_ctx->rc_max_rate = (int64_t)kbps * 1500; + m_ctx->rc_buffer_size = (int)(kbps * 1000); + // 注意:FFmpeg 多数硬编不支持运行时改 bit_rate 让 ctx 立即生效; + // 这里只更新数值,下次 open 时才生效。Step 1 不依赖动态调码率。 +} + +int CFFmpegH264Encoder::convertRGB24ToNV12(uint8_t* rgb, uint32_t stride, + uint32_t width, uint32_t height, + int direction) +{ + int signed_height = direction * (int)height; + int w = (int)width; + int h = (int)height; + int y_size = w * h; + int uv_size = (w / 2) * (h / 2); + m_i420Scratch.resize(y_size + 2 * uv_size); + + uint8_t* y = m_i420Scratch.data(); + uint8_t* u = y + y_size; + uint8_t* v = u + uv_size; + + if (libyuv::RGB24ToI420( + rgb, stride, + y, w, + u, w / 2, + v, w / 2, + w, signed_height) != 0) { + return -1; + } + if (libyuv::I420ToNV12( + y, w, + u, w / 2, + v, w / 2, + m_frame->data[0], m_frame->linesize[0], + m_frame->data[1], m_frame->linesize[1], + w, h) != 0) { + return -1; + } + return 0; +} + +int CFFmpegH264Encoder::encode( + uint8_t* rgb, uint8_t bpp, uint32_t stride, + uint32_t width, uint32_t height, + uint8_t** lppData, uint32_t* lpSize, int direction) +{ + if (!m_ctx || !m_frame || !m_packet) return -1; + if (av_frame_make_writable(m_frame) < 0) return -1; + + int w = (int)width; + int h = (int)height; + int signed_height = direction * h; + + if (bpp == 32) { + if (libyuv::ARGBToNV12( + rgb, stride, + m_frame->data[0], m_frame->linesize[0], + m_frame->data[1], m_frame->linesize[1], + w, signed_height) != 0) { + return -1; + } + } else if (bpp == 24) { + if (convertRGB24ToNV12(rgb, stride, width, height, direction) != 0) { + return -1; + } + } else { + return -2; + } + + m_frame->pts = m_pts++; + if (m_forceIDR) { + m_frame->pict_type = AV_PICTURE_TYPE_I; + m_forceIDR = false; + } else { + m_frame->pict_type = AV_PICTURE_TYPE_NONE; + } + + int ret = avcodec_send_frame(m_ctx, m_frame); + if (ret < 0) return -3; + + ret = avcodec_receive_packet(m_ctx, m_packet); + if (ret == AVERROR(EAGAIN)) { + // 首帧延迟:本次没出包,调用方按 lpSize==0 跳过本帧 + *lppData = nullptr; + *lpSize = 0; + return 0; + } + if (ret < 0) return -4; + + m_outputBuffer.assign(m_packet->data, m_packet->data + m_packet->size); + *lppData = m_outputBuffer.data(); + *lpSize = (uint32_t)m_outputBuffer.size(); + av_packet_unref(m_packet); + return 0; +} + +#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST diff --git a/client/CFFmpegH264Encoder.h b/client/CFFmpegH264Encoder.h new file mode 100644 index 0000000..70cc33c --- /dev/null +++ b/client/CFFmpegH264Encoder.h @@ -0,0 +1,62 @@ +#pragma once + +#include "VideoEncoderBase.h" +#include "common/config.h" +#include +#include + +// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整类移出编译单元,避免 GPL 传染(与 c0a632a 对齐) +#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST + +struct AVCodecContext; +struct AVFrame; +struct AVPacket; + +// FFmpeg 硬编 H.264 实现。 +// Step 1: 仅探测 h264_nvenc 单后端,足以验证 FFmpeg 静态库集成链路。 +// Step 2: 扩展 h264_qsv / h264_amf / h264_mf。 +// +// 输入像素:BGRA (bpp=32) / RGB24 (bpp=24),与 CX264Encoder 完全一致; +// 内部转 NV12 喂给 FFmpeg encoder。 +class CFFmpegH264Encoder : public VideoEncoderBase +{ +public: + CFFmpegH264Encoder(); + ~CFFmpegH264Encoder() override; + + bool open(const EncoderParams& params) override; + void close() override; + + int encode( + uint8_t* rgb, + uint8_t bpp, + uint32_t stride, + uint32_t width, + uint32_t height, + uint8_t** lppData, + uint32_t* lpSize, + int direction = 1 + ) override; + + void forceIDR() override { m_forceIDR = true; } + void setBitrate(int kbps) override; + VideoCodec codec() const override { return VideoCodec::H264; } + const char* backendName() const override { return m_backend.c_str(); } + +private: + bool tryOpenBackend(const char* name, const EncoderParams& p); + void cleanupCodec(); + int convertRGB24ToNV12(uint8_t* rgb, uint32_t stride, + uint32_t width, uint32_t height, int direction); + + AVCodecContext* m_ctx = nullptr; + AVFrame* m_frame = nullptr; + AVPacket* m_packet = nullptr; + std::vector m_outputBuffer; // encode 返回给调用方的缓冲(持有到下一次 encode) + std::vector m_i420Scratch; // RGB24 路径的中间缓冲 + int64_t m_pts = 0; + bool m_forceIDR = false; + std::string m_backend; // 实际选中的后端名("h264_nvenc" / ...) +}; + +#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST diff --git a/client/ClientDll_vs2015.vcxproj b/client/ClientDll_vs2015.vcxproj index 540d628..563d04f 100644 --- a/client/ClientDll_vs2015.vcxproj +++ b/client/ClientDll_vs2015.vcxproj @@ -124,7 +124,7 @@ zlib\zlib_x64.lib;%(AdditionalDependencies) libcmt.lib /ignore:4099 %(AdditionalOptions) - $(SolutionDir)..\SimplePlugins\bin + $(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib @@ -167,7 +167,7 @@ true zlib\zlib_x64.lib;%(AdditionalDependencies) /SAFESEH:NO /ignore:4099 %(AdditionalOptions) - $(SolutionDir)..\SimplePlugins\bin + $(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib @@ -205,6 +205,9 @@ + + + @@ -228,6 +231,10 @@ + + + + diff --git a/client/ClientDll_vs2015.vcxproj.filters b/client/ClientDll_vs2015.vcxproj.filters index f3c60ed..d20f773 100644 --- a/client/ClientDll_vs2015.vcxproj.filters +++ b/client/ClientDll_vs2015.vcxproj.filters @@ -36,6 +36,9 @@ + + + @@ -81,6 +84,10 @@ + + + + diff --git a/client/EncoderFactory.cpp b/client/EncoderFactory.cpp new file mode 100644 index 0000000..70ff97b --- /dev/null +++ b/client/EncoderFactory.cpp @@ -0,0 +1,71 @@ +#include "EncoderFactory.h" +#include "common/config.h" +#include "common/logger.h" +#include "X264Encoder.h" +// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时硬编实现整体移出工程,仅保留 x264 软编路径 +#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST +#include "CFFmpegH264Encoder.h" +#include "CFFmpegAV1Encoder.h" +#endif + +namespace { +// 与 ScreenCapture::BitRateToCRF 同步:码率越高 CRF 越低(质量更好)。 +// 仅 x264 软编路径用,硬编路径直接用 bitrate_kbps 走 CBR。 +int BitRateToCRF(int bitRate) { + if (bitRate <= 0) return 23; + if (bitRate >= 3000) return 20; + if (bitRate >= 2000) return 20 + (3000 - bitRate) * 3 / 1000; + if (bitRate >= 800) return 23 + (2000 - bitRate) * 7 / 1200; + return 32; +} +} + +std::unique_ptr CreateEncoder(const EncoderRequest& req) { + EncoderParams p; + p.width = req.width; + p.height = req.height; + p.fps = req.fps; + +#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST + // AV1 硬编路径(仅当客户端声明支持 AV1 解码) + // 硬件门槛高:仅 RTX 40+ / RX 7000+ / Intel Arc 才有 av1 encoder ASIC; + // 没合适硬件时 open() 全部失败,自然 fall through 到下面 H.264 路径。 + if (req.encodeLevel >= LEVEL_AV1_HARD) { + auto enc = std::make_unique(); + p.rc = RateControl::BITRATE; + p.bitrate_kbps = req.bitrate_kbps; + if (enc->open(p)) { + Mprintf("=> encoder: %s (HW AV1, bitrate=%dk)\n", enc->backendName(), req.bitrate_kbps); + return enc; + } + Mprintf("=> all AV1 HW backends failed, falling back to H.264\n"); + } + + + // H.264 硬编:CFFmpegH264Encoder 内部按 nvenc/qsv/amf/mf 顺序探 + if (req.encodeLevel >= LEVEL_H264_HARD) { + auto enc = std::make_unique(); + p.rc = RateControl::BITRATE; + p.bitrate_kbps = req.bitrate_kbps; + if (enc->open(p)) { + Mprintf("=> encoder: %s (HW, bitrate=%dk)\n", enc->backendName(), req.bitrate_kbps); + return enc; + } + Mprintf("=> all H.264 HW backends failed, falling back to x264\n"); + } +#endif + + // x264 软编兜底(无硬件 / 全失败 / 虚拟机 / 远程桌面会话场景) + if (req.encodeLevel >= LEVEL_H264_SOFT) { + auto enc = std::make_unique(); + p.rc = RateControl::CRF; + p.crf = BitRateToCRF(req.bitrate_kbps); + if (enc->open(p)) { + Mprintf("=> encoder: %s (SW, crf=%d)\n", enc->backendName(), p.crf); + return enc; + } + } + + Mprintf("=> ERROR: no encoder could be opened\n"); + return nullptr; +} diff --git a/client/EncoderFactory.h b/client/EncoderFactory.h new file mode 100644 index 0000000..291c779 --- /dev/null +++ b/client/EncoderFactory.h @@ -0,0 +1,25 @@ +#pragma once + +#include "VideoEncoderBase.h" +#include "common/commands.h" +#include + + +// 创建编码器的请求参数。 +struct EncoderRequest { + int width = 0; + int height = 0; + int fps = 30; + int bitrate_kbps = 4000; + int encodeLevel = LEVEL_H264_SOFT; +}; + +// 按客户端能力 + 本机硬件能力创建一个 VideoEncoderBase。 +// +// 探测顺序(第一个 open 成功的就用): +// AV1 硬编路径 +// H.264 硬编(CFFmpegH264Encoder 内部按 nvenc/qsv/amf/mf 探) +// x264 软编(CX264Encoder,CPU 兜底) +// +// 失败路径在日志中可见(Mprintf)。返回 nullptr 仅在 x264 也开不起来时(极少见)。 +std::unique_ptr CreateEncoder(const EncoderRequest& req); diff --git a/client/ScreenCapture.h b/client/ScreenCapture.h index 7a4b778..ffa4da2 100644 --- a/client/ScreenCapture.h +++ b/client/ScreenCapture.h @@ -13,8 +13,11 @@ #include #include #include +#include #include // SSE2 -#include "X264Encoder.h" +#include "common/config.h" +#include "VideoEncoderBase.h" +#include "EncoderFactory.h" #include "ScrollDetector.h" #include "common/file_upload.h" @@ -126,6 +129,7 @@ public: ULONG* m_BlockSizes; // 分块差异像素数 int m_BlockNum; // 分块个数 int m_SendQuality; // 发送质量 + int m_EncodeLevel; // 编码级别 LPBITMAPINFO m_BitmapInfor_Full; // BMP信息 LPBITMAPINFO m_BitmapInfor_Send; // 发送的BMP信息 @@ -145,7 +149,7 @@ public: int m_FrameID; // 帧序号 int m_GOP; // 关键帧间隔 bool m_SendKeyFrame; // 发送关键帧 - CX264Encoder *m_encoder; // 编码器 + std::unique_ptr m_encoder; // 编码器,ensureEncoder() lazy 创建,走 EncoderFactory 探测 int m_nScreenCount; // 屏幕数量 BOOL m_bEnableMultiScreen;// 多显示器支持 @@ -182,14 +186,14 @@ protected: int m_nVScreenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN); public: - ScreenCapture(int n = 32, BYTE algo = ALGORITHM_DIFF, BOOL all = FALSE) : + ScreenCapture(int n = 32, BYTE algo = ALGORITHM_DIFF, BOOL all = FALSE, int level = LEVEL_H264_SOFT) : m_ThreadPool(nullptr), m_FirstBuffer(nullptr), m_RectBuffer(nullptr), m_BitmapInfor_Full(nullptr), m_bAlgorithm(algo), m_SendQuality(100), m_ulFullWidth(0), m_ulFullHeight(0), m_bZoomed(false), m_wZoom(1), m_hZoom(1), m_FrameID(0), m_GOP(DEFAULT_GOP), m_iScreenX(0), m_iScreenY(0), m_biBitCount(n), m_SendKeyFrame(false), m_encoder(nullptr), m_pScrollDetector(nullptr), m_bEnableScrollDetect(false), m_bServerSupportsScroll(false), - m_bLastFrameWasScroll(false), m_nScrollDetectInterval(1) + m_bLastFrameWasScroll(false), m_nScrollDetectInterval(1), m_EncodeLevel(level) { SetAlgorithm(algo); m_BitmapInfor_Send = nullptr; @@ -256,7 +260,6 @@ public: SAFE_DELETE_ARRAY(m_BlockSizes); SAFE_DELETE(m_ThreadPool); - SAFE_DELETE(m_encoder); SAFE_DELETE(m_pScrollDetector); } @@ -839,6 +842,19 @@ public: return bmpInfo; } + // 编码器 lazy 创建。委托 EncoderFactory 完成"硬编探测 + 软编 fallback"。 + void ensureEncoder(int width, int height) + { + if (m_encoder) return; + EncoderRequest req; + req.width = width; + req.height = height; + req.fps = 20; + req.bitrate_kbps = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266); + req.encodeLevel = m_EncodeLevel; + m_encoder = CreateEncoder(req); + } + // 算法+光标位置+光标类型 virtual LPBYTE GetNextScreenData(ULONG* ulNextSendLength) { @@ -923,13 +939,12 @@ public: uint8_t* encoded_data = nullptr; uint32_t encoded_size = 0; int width = m_BitmapInfor_Send->bmiHeader.biWidth, height = m_BitmapInfor_Send->bmiHeader.biHeight; - if (m_encoder == nullptr) { - m_encoder = new CX264Encoder(); - int br = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266); - m_encoder->open(width, height, 20, BitRateToCRF(br)); - } + ensureEncoder(width, height); + if (!m_encoder) return nullptr; + m_encoder->forceIDR(); // 协议层 keyframe → 编码器强制 IDR,与 TOKEN_KEYFRAME 语义对齐 int err = m_encoder->encode(nextData, 32, 4 * width, width, height, &encoded_data, &encoded_size); - if (err) { + // encoded_size == 0:硬编首帧延迟(avcodec_receive_packet 返回 EAGAIN),本帧无码流,按失败跳过 + if (err || encoded_size == 0) { return nullptr; } *ulNextSendLength = 1 + offset + encoded_size; @@ -953,13 +968,11 @@ public: uint8_t* encoded_data = nullptr; uint32_t encoded_size = 0; int width = m_BitmapInfor_Send->bmiHeader.biWidth, height = m_BitmapInfor_Send->bmiHeader.biHeight; - if (m_encoder == nullptr) { - m_encoder = new CX264Encoder(); - int br = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266); - m_encoder->open(width, height, 20, BitRateToCRF(br)); - } + ensureEncoder(width, height); + if (!m_encoder) return nullptr; int err = m_encoder->encode(nextData, 32, 4 * width, width, height, &encoded_data, &encoded_size); - if (err) { + // encoded_size == 0:硬编首帧延迟,本帧无码流,按失败跳过 + if (err || encoded_size == 0) { return nullptr; } *ulNextSendLength = 1 + offset + encoded_size; diff --git a/client/ScreenCapturerDXGI.h b/client/ScreenCapturerDXGI.h index 8fe3928..6ee4d1f 100644 --- a/client/ScreenCapturerDXGI.h +++ b/client/ScreenCapturerDXGI.h @@ -25,7 +25,8 @@ private: BYTE* m_NextBuffer = nullptr; public: - ScreenCapturerDXGI(BYTE algo, int gop = DEFAULT_GOP, BOOL all = FALSE) : ScreenCapture(32, algo, all) + ScreenCapturerDXGI(BYTE algo, int gop = DEFAULT_GOP, BOOL all = FALSE, int level = LEVEL_H264_SOFT) + : ScreenCapture(32, algo, all, level) { m_GOP = gop; InitDXGI(all); diff --git a/client/ScreenManager.cpp b/client/ScreenManager.cpp index a085400..40449c7 100644 --- a/client/ScreenManager.cpp +++ b/client/ScreenManager.cpp @@ -154,6 +154,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", quality); m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0); m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频 + m_ScreenSettings.EncodeLevel = cfg.GetInt("settings", "EncodeLevel", LEVEL_H264_SOFT); LoadQualityProfiles(); // 加载质量配置 @@ -519,18 +520,18 @@ void CScreenManager::InitScreenSpy() SAFE_DELETE(m_ScreenSpyObject); if ((USING_DXGI == DXGI && IsWindows8orHigher())) { m_isGDI = FALSE; - auto s = new ScreenCapturerDXGI(algo, DEFAULT_GOP, all); + auto s = new ScreenCapturerDXGI(algo, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel); if (s->IsInitSucceed()) { m_ScreenSpyObject = s; } else { SAFE_DELETE(s); m_isGDI = TRUE; - m_ScreenSpyObject = new CScreenSpy(32, algo, FALSE, DEFAULT_GOP, all); + m_ScreenSpyObject = new CScreenSpy(32, algo, FALSE, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel); Mprintf("CScreenManager: DXGI SPY init failed!!! Using GDI instead.\n"); } } else { m_isGDI = TRUE; - m_ScreenSpyObject = new CScreenSpy(32, algo, DXGI == USING_VIRTUAL, DEFAULT_GOP, all); + m_ScreenSpyObject = new CScreenSpy(32, algo, DXGI == USING_VIRTUAL, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel); } } @@ -817,6 +818,14 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength) m_ClientObject->StopRunning(); break; } + case COMMAND_ENCODE_LEVEL: { + int encodeLevel = szBuffer[1]; + iniFile cfg(CLIENT_PATH); + cfg.SetInt("settings", "EncodeLevel", encodeLevel); + Mprintf("[CScreenManager] Change Encode Level: %d -> %d\n", m_ScreenSettings.EncodeLevel, encodeLevel); + m_ScreenSettings.EncodeLevel = encodeLevel; + break; + } case COMMAND_SWITCH_SCREEN: { SwitchScreen(); break; diff --git a/client/ScreenSpy.cpp b/client/ScreenSpy.cpp index 83a061b..a90d313 100644 --- a/client/ScreenSpy.cpp +++ b/client/ScreenSpy.cpp @@ -12,8 +12,8 @@ // Construction/Destruction ////////////////////////////////////////////////////////////////////// -CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL all) : - ScreenCapture(ulbiBitCount, algo, all) +CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL all, int level) : + ScreenCapture(ulbiBitCount, algo, all, level) { m_GOP = gop; diff --git a/client/ScreenSpy.h b/client/ScreenSpy.h index 8980e8f..2da8b5f 100644 --- a/client/ScreenSpy.h +++ b/client/ScreenSpy.h @@ -97,7 +97,7 @@ protected: EnumHwndsPrintData m_data; public: - CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk = FALSE, int gop = DEFAULT_GOP, BOOL all = FALSE); + CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk = FALSE, int gop = DEFAULT_GOP, BOOL all = FALSE, int level = LEVEL_H264_SOFT); virtual ~CScreenSpy(); diff --git a/client/VideoEncoderBase.h b/client/VideoEncoderBase.h new file mode 100644 index 0000000..8eef802 --- /dev/null +++ b/client/VideoEncoderBase.h @@ -0,0 +1,59 @@ +#pragma once +#include + +// 视频编码器抽象接口 +// Step 0: 仅 CX264Encoder 实现;后续 CFFmpegH264Encoder / CFFmpegAV1Encoder 接入 +// 详见 docs/HardwareEncoding_Design.md + +enum class VideoCodec { + H264, + AV1, +}; + +enum class RateControl { + CRF, // x264 软编用 CRF (0-51, 越小越好) + BITRATE, // 硬编路径用目标码率 (kbps) +}; + +struct EncoderParams { + int width = 0; + int height = 0; + int fps = 30; + RateControl rc = RateControl::BITRATE; + int crf = 23; // 当 rc == CRF + int bitrate_kbps = 4000; // 当 rc == BITRATE + int gop_seconds = 15; // 关键帧间隔(秒),与 x264 i_keyint_max=fps*15 对齐 +}; + +class VideoEncoderBase { +public: + virtual ~VideoEncoderBase() = default; + + virtual bool open(const EncoderParams& params) = 0; + virtual void close() = 0; + + // 编码一帧 + // rgb : 输入像素数据 + // bpp : 24 (RGB) / 32 (BGRA) + // stride : 源行字节数 + // width/height : 图像尺寸 + // lppData : 输出指针,指向编码后码流(生命周期归编码器,下一次 encode 失效) + // lpSize : 输出码流字节数;返回 0 表示成功但本帧无输出(硬编首帧延迟) + // direction : 1 = 上下不翻转,-1 = 翻转(适配 Windows BMP bottom-up) + // 返回 0 = 成功;< 0 = 失败 + virtual int encode( + uint8_t* rgb, + uint8_t bpp, + uint32_t stride, + uint32_t width, + uint32_t height, + uint8_t** lppData, + uint32_t* lpSize, + int direction = 1 + ) = 0; + + virtual void forceIDR() = 0; + virtual void setBitrate(int kbps) {} // 可选实现,默认 no-op + virtual VideoCodec codec() const = 0; + virtual const char* backendName() const = 0; // "x264" / "h264_nvenc" / "av1_amf" ... +}; diff --git a/client/X264Encoder.cpp b/client/X264Encoder.cpp index 783e4f2..711adc7 100644 --- a/client/X264Encoder.cpp +++ b/client/X264Encoder.cpp @@ -3,10 +3,11 @@ #include #if DISABLE_X264_FOR_TEST -CX264Encoder::CX264Encoder() { memset(&m_Param, 0, sizeof(m_Param)); m_pCodec = NULL; m_pPicIn = NULL; m_pPicOut = NULL; } +CX264Encoder::CX264Encoder() { memset(&m_Param, 0, sizeof(m_Param)); m_pCodec = NULL; m_pPicIn = NULL; m_pPicOut = NULL; m_forceIDR = false; } CX264Encoder::~CX264Encoder() {} bool CX264Encoder::open(int, int, int, int) { return false; } bool CX264Encoder::open(x264_param_t*) { return false; } +bool CX264Encoder::open(const EncoderParams&) { return false; } void CX264Encoder::close() {} int CX264Encoder::encode(uint8_t*, uint8_t, uint32_t, uint32_t, uint32_t, uint8_t**, uint32_t*, int) { return -1; } @@ -25,6 +26,7 @@ CX264Encoder::CX264Encoder() m_pCodec = NULL; m_pPicIn = NULL; m_pPicOut = NULL; + m_forceIDR = false; } @@ -88,6 +90,14 @@ bool CX264Encoder::open(x264_param_t * param) } +bool CX264Encoder::open(const EncoderParams& params) +{ + // x264 软编只支持 CRF;调用方走 BITRATE 时降级为 CRF=23(与 BitRateToCRF 默认一致) + int crf = (params.rc == RateControl::CRF) ? params.crf : 23; + return open(params.width, params.height, params.fps, crf); +} + + void CX264Encoder::close() { if (m_pCodec) { @@ -146,6 +156,12 @@ int CX264Encoder::encode( return -2; } + if (m_forceIDR) { + m_pPicIn->i_type = X264_TYPE_IDR; + m_forceIDR = false; + } else { + m_pPicIn->i_type = X264_TYPE_AUTO; + } encode_size = x264_encoder_encode( m_pCodec, diff --git a/client/X264Encoder.h b/client/X264Encoder.h index 0be2592..356a63b 100644 --- a/client/X264Encoder.h +++ b/client/X264Encoder.h @@ -1,5 +1,7 @@ #pragma once +#include "VideoEncoderBase.h" + extern "C" { #include #include @@ -7,19 +9,22 @@ extern "C" { #include "common/config.h" -class CX264Encoder +class CX264Encoder : public VideoEncoderBase { private: x264_t* m_pCodec; //编码器实例 x264_picture_t *m_pPicIn; x264_picture_t *m_pPicOut; x264_param_t m_Param; + bool m_forceIDR; // 下一次 encode 强制 IDR public: + // 旧签名保留:被 ScreenCapture 临时直接调;新增 EncoderParams overload 走接口路径 bool open(int width, int height, int fps, int crf); bool open(x264_param_t * param); - void close(); - + // VideoEncoderBase + bool open(const EncoderParams& params) override; + void close() override; int encode( uint8_t * rgb, uint8_t bpp, @@ -29,9 +34,11 @@ public: uint8_t ** lppData, uint32_t * lpSize, int direction = 1 - ); + ) override; + void forceIDR() override { m_forceIDR = true; } + VideoCodec codec() const override { return VideoCodec::H264; } + const char* backendName() const override { return "x264"; } CX264Encoder(); - ~CX264Encoder(); + ~CX264Encoder() override; }; - diff --git a/client/ghost_vs2015.vcxproj b/client/ghost_vs2015.vcxproj index e42775d..2b7bb88 100644 --- a/client/ghost_vs2015.vcxproj +++ b/client/ghost_vs2015.vcxproj @@ -130,7 +130,7 @@ Console /ignore:4099 %(AdditionalOptions) - $(SolutionDir)..\SimplePlugins\bin + $(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib @@ -177,7 +177,7 @@ /SAFESEH:NO /ignore:4099 %(AdditionalOptions) Windows mainCRTStartup - $(SolutionDir)..\SimplePlugins\bin + $(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib @@ -218,6 +218,9 @@ + + + @@ -266,7 +269,11 @@ + + + + diff --git a/common/commands.h b/common/commands.h index ee0939a..3311f0d 100644 --- a/common/commands.h +++ b/common/commands.h @@ -255,6 +255,7 @@ enum { CMD_AUDIO_CTRL = 95, // 音频控制: [cmd:1][enable:1][persist:1] TOKEN_SCREEN_AUDIO = 96, // 音频数据: [token:1][hasFormat:1][AudioFormat?][data] COMMAND_SHARE_CANCEL = 97, + COMMAND_ENCODE_LEVEL = 98, TOKEN_SCROLL_FRAME = 99, // 滚动优化帧 // 服务端发出的标识 @@ -1188,6 +1189,12 @@ enum QualityLevel { #define ALGORITHM_RGB565 3 // RGB565 压缩 #endif +enum EncodeLevel { + LEVEL_H264_SOFT = 0, + LEVEL_H264_HARD = 1, + LEVEL_AV1_HARD = 2, +}; + /* 质量配置(与 QualityLevel 对应) - strategy = 0:1080p 限制 - strategy = 1:原始分辨率 @@ -1272,7 +1279,8 @@ typedef struct ScreenSettings { int CpuSpeedup; // 偏移 36, 指令集加速(0: 无, 1: SSE2) int ScreenType; // 偏移 40, 屏幕类型(0: GDI, 1: DXGI, 2: Virtual) int AudioEnabled; // 偏移 44, 音频传输(0: 禁用, 1: 启用) - char Reserved[48]; // 偏移 48, 保留字段(新能力参数从此处扩展) + int EncodeLevel; // 偏移 48, 编码等级 + char Reserved[44]; // 偏移 52, 保留字段(新能力参数从此处扩展) uint32_t Capabilities; // 偏移 96, 能力位标志(放最后) } ScreenSettings; // 总大小 100 字节 diff --git a/compress/ffmpeg/dav1d_x64.lib b/compress/ffmpeg/dav1d_x64.lib new file mode 100644 index 0000000..e24573d Binary files /dev/null and b/compress/ffmpeg/dav1d_x64.lib differ diff --git a/compress/ffmpeg/vpl_x64.lib b/compress/ffmpeg/vpl_x64.lib new file mode 100644 index 0000000..51e3fed Binary files /dev/null and b/compress/ffmpeg/vpl_x64.lib differ diff --git a/compress/ffmpeg/vpl_x64d.lib b/compress/ffmpeg/vpl_x64d.lib new file mode 100644 index 0000000..25f5144 Binary files /dev/null and b/compress/ffmpeg/vpl_x64d.lib differ diff --git a/docs/HardwareEncoding_Design.md b/docs/HardwareEncoding_Design.md new file mode 100644 index 0000000..67168fb --- /dev/null +++ b/docs/HardwareEncoding_Design.md @@ -0,0 +1,977 @@ +# 视频编码硬件加速实现指导文档 + +本文档供 AI 编码助手参考,用于在现有 C++ 远程控制程序中实现 H.264 硬件编码 + AV1 编码路径。 + +--- + +## 1. 项目背景 + +### 1.1 当前状态 + +- C++ Windows 远程控制程序 +- 已实现 H.264 编码,基于 x264 软编(`CX264Encoder`),preset = `ultrafast + zerolatency` +- 视频管线:桌面捕获(RGB/BGRA)→ 编码 → 网络传输 → 客户端解码显示 +- 当前架构:每个主控端连接对应一个独立编码器实例 +- **分发模式**:单 exe,FFmpeg 静态链接 + +### 1.2 目标 + +分两阶段渐进推进,**始终保留 x264 软编作为兜底**: + +**阶段一(H.264 硬编加速)** +- 新增 H.264 硬编(NVENC / QSV / AMF),按 GPU 能力探测优先走硬编 +- x264 软编在无 GPU / 虚拟机 / 远程桌面会话等环境下兜底 +- 浏览器解码零兼容性风险(H.264 全平台原生支持) + +**阶段二(AV1 路径)** +- 新增 AV1 硬编(`av1_nvenc` / `av1_qsv` / `av1_amf`) +- 客户端浏览器握手时声明 AV1 能力 +- 双方都能用就走 AV1,否则回落 H.264 + +**最终产物仍为单 exe**,体积增量可接受 6–10 MB。 + +### 1.3 关键决策记录 + +#### 1.3.1 为什么跳过 HEVC + +经评估,HEVC 在本项目目标场景下没有独占价值: + +| 维度 | 现状 | +|---|---| +| **浏览器解码** | Firefox 完全不支持;Chrome/Edge 需 Win11 + 商店付费的 HEVC Video Extensions | +| **专利授权** | 商用涉及 MPEG-LA / Access Advance / Velos Media 三个专利池 | +| **替代方案** | AV1 压缩效率更高、AOMedia 免专利、浏览器原生支持广 | + +HEVC 编码端硬件普及度好(几乎所有 2015+ GPU)这个优势,被解码端短板完全抵消。 + +#### 1.3.2 为什么 H.264 硬编先于 AV1 + +- **AV1 硬编硬件门槛高**:仅 NVIDIA RTX 40+ / AMD RX 7000+ / Intel Arc 才有 +- **"多机混杂"场景**下大部分编码端 GPU 没有 AV1 硬编 +- **H.264 硬编**(NVENC/QSV/AMF)几乎所有现代 GPU 都有,覆盖面广 +- **客户端浏览器解 H.264** 是零兼容性问题,跨浏览器/跨平台 100% 通用 + +H.264 硬编是先把"地板抬起来",AV1 是"在新硬件上的天花板"。 + +### 1.4 设计约束 + +- **平台**:仅 Windows(macOS/Linux 未来另行设计) +- **GPU 不确定**:NVIDIA / AMD / Intel / 无独显 / 虚拟机无 GPU 都需支持 +- **延迟要求**:不敏感(不追求极致低延迟) +- **并发模型**:通常 1 对 1,少数 1 对多(每个连接独立编码器) +- **客户端**:浏览器(WebCodecs 优先,`