Feature: Implement H.264 and AV1 hardware encoding for remote control
Remark: Need to update FFmpeg static libraries to take effort
This commit is contained in:
243
client/CFFmpegAV1Encoder.cpp
Normal file
243
client/CFFmpegAV1Encoder.cpp
Normal file
@@ -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 <libavcodec/avcodec.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
#include <libyuv/libyuv.h>
|
||||
}
|
||||
|
||||
#include <string.h>
|
||||
|
||||
// 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
|
||||
62
client/CFFmpegAV1Encoder.h
Normal file
62
client/CFFmpegAV1Encoder.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoEncoderBase.h"
|
||||
#include "common/config.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 合规守护: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<uint8_t> m_outputBuffer;
|
||||
std::vector<uint8_t> m_i420Scratch;
|
||||
int64_t m_pts = 0;
|
||||
bool m_forceIDR = false;
|
||||
std::string m_backend;
|
||||
};
|
||||
|
||||
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
|
||||
299
client/CFFmpegH264Encoder.cpp
Normal file
299
client/CFFmpegH264Encoder.cpp
Normal file
@@ -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 <libavcodec/avcodec.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
#include <libyuv/libyuv.h>
|
||||
}
|
||||
|
||||
#include <string.h>
|
||||
#include <cstdlib>
|
||||
|
||||
// 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
|
||||
62
client/CFFmpegH264Encoder.h
Normal file
62
client/CFFmpegH264Encoder.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoEncoderBase.h"
|
||||
#include "common/config.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 合规守护: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<uint8_t> m_outputBuffer; // encode 返回给调用方的缓冲(持有到下一次 encode)
|
||||
std::vector<uint8_t> m_i420Scratch; // RGB24 路径的中间缓冲
|
||||
int64_t m_pts = 0;
|
||||
bool m_forceIDR = false;
|
||||
std::string m_backend; // 实际选中的后端名("h264_nvenc" / ...)
|
||||
};
|
||||
|
||||
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
|
||||
@@ -124,7 +124,7 @@
|
||||
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<IgnoreSpecificDefaultLibraries>libcmt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
|
||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
@@ -167,7 +167,7 @@
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalOptions> /SAFESEH:NO /ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
|
||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
@@ -205,6 +205,9 @@
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
<ClCompile Include="StdAfx.cpp" />
|
||||
<ClCompile Include="SystemManager.cpp" />
|
||||
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
|
||||
<ClCompile Include="CFFmpegH264Encoder.cpp" />
|
||||
<ClCompile Include="EncoderFactory.cpp" />
|
||||
<ClCompile Include="TalkManager.cpp" />
|
||||
<ClCompile Include="VideoManager.cpp" />
|
||||
<ClCompile Include="X264Encoder.cpp" />
|
||||
@@ -228,6 +231,10 @@
|
||||
<ClInclude Include="IOCPClient.h" />
|
||||
<ClInclude Include="IOCPKCPClient.h" />
|
||||
<ClInclude Include="IOCPUDPClient.h" />
|
||||
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||
<ClInclude Include="CFFmpegH264Encoder.h" />
|
||||
<ClInclude Include="EncoderFactory.h" />
|
||||
<ClInclude Include="VideoEncoderBase.h" />
|
||||
<ClInclude Include="KernelManager.h" />
|
||||
<ClInclude Include="KeyboardManager.h" />
|
||||
<ClInclude Include="keylogger.h" />
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<ClCompile Include="TalkManager.cpp" />
|
||||
<ClCompile Include="VideoManager.cpp" />
|
||||
<ClCompile Include="X264Encoder.cpp" />
|
||||
<ClCompile Include="CFFmpegH264Encoder.cpp" />
|
||||
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
|
||||
<ClCompile Include="EncoderFactory.cpp" />
|
||||
<ClCompile Include="..\common\file_upload.cpp" />
|
||||
<ClCompile Include="ConPTYManager.cpp" />
|
||||
</ItemGroup>
|
||||
@@ -81,6 +84,10 @@
|
||||
<ClInclude Include="VideoCodec.h" />
|
||||
<ClInclude Include="VideoManager.h" />
|
||||
<ClInclude Include="X264Encoder.h" />
|
||||
<ClInclude Include="VideoEncoderBase.h" />
|
||||
<ClInclude Include="CFFmpegH264Encoder.h" />
|
||||
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||
<ClInclude Include="EncoderFactory.h" />
|
||||
<ClInclude Include="ConPTYManager.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
71
client/EncoderFactory.cpp
Normal file
71
client/EncoderFactory.cpp
Normal file
@@ -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<VideoEncoderBase> 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<CFFmpegAV1Encoder>();
|
||||
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<CFFmpegH264Encoder>();
|
||||
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<CX264Encoder>();
|
||||
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;
|
||||
}
|
||||
25
client/EncoderFactory.h
Normal file
25
client/EncoderFactory.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoEncoderBase.h"
|
||||
#include "common/commands.h"
|
||||
#include <memory>
|
||||
|
||||
|
||||
// 创建编码器的请求参数。
|
||||
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<VideoEncoderBase> CreateEncoder(const EncoderRequest& req);
|
||||
@@ -13,8 +13,11 @@
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <emmintrin.h> // 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<VideoEncoderBase> 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
59
client/VideoEncoderBase.h
Normal file
59
client/VideoEncoderBase.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// 视频编码器抽象接口
|
||||
// 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" ...
|
||||
};
|
||||
@@ -3,10 +3,11 @@
|
||||
#include <stdio.h>
|
||||
|
||||
#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,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoEncoderBase.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libyuv\libyuv.h>
|
||||
#include <x264\x264.h>
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
</EntryPointSymbol>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
|
||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
@@ -177,7 +177,7 @@
|
||||
<AdditionalOptions> /SAFESEH:NO /ignore:4099 %(AdditionalOptions)</AdditionalOptions>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
|
||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories>
|
||||
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
@@ -218,6 +218,9 @@
|
||||
<ClCompile Include="ConPTYManager.cpp" />
|
||||
<ClCompile Include="StdAfx.cpp" />
|
||||
<ClCompile Include="SystemManager.cpp" />
|
||||
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
|
||||
<ClCompile Include="CFFmpegH264Encoder.cpp" />
|
||||
<ClCompile Include="EncoderFactory.cpp" />
|
||||
<ClCompile Include="TalkManager.cpp" />
|
||||
<ClCompile Include="VideoManager.cpp" />
|
||||
<ClCompile Include="X264Encoder.cpp" />
|
||||
@@ -266,7 +269,11 @@
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
<ClInclude Include="ConPTYManager.h" />
|
||||
<ClInclude Include="StdAfx.h" />
|
||||
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||
<ClInclude Include="CFFmpegH264Encoder.h" />
|
||||
<ClInclude Include="EncoderFactory.h" />
|
||||
<ClInclude Include="SystemManager.h" />
|
||||
<ClInclude Include="VideoEncoderBase.h" />
|
||||
<ClInclude Include="TalkManager.h" />
|
||||
<ClInclude Include="VideoCodec.h" />
|
||||
<ClInclude Include="VideoManager.h" />
|
||||
|
||||
Reference in New Issue
Block a user