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:
yuanyuanxiang
2026-05-28 11:41:33 +02:00
parent d1aa7a2c02
commit 8c7f612449
30 changed files with 2113 additions and 68 deletions

View File

@@ -0,0 +1,977 @@
# 视频编码硬件加速实现指导文档
本文档供 AI 编码助手参考,用于在现有 C++ 远程控制程序中实现 H.264 硬件编码 + AV1 编码路径。
---
## 1. 项目背景
### 1.1 当前状态
- C++ Windows 远程控制程序
- 已实现 H.264 编码,基于 x264 软编(`CX264Encoder`preset = `ultrafast + zerolatency`
- 视频管线桌面捕获RGB/BGRA→ 编码 → 网络传输 → 客户端解码显示
- 当前架构:每个主控端连接对应一个独立编码器实例
- **分发模式**:单 exeFFmpeg 静态链接
### 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**,体积增量可接受 610 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 设计约束
- **平台**:仅 WindowsmacOS/Linux 未来另行设计)
- **GPU 不确定**NVIDIA / AMD / Intel / 无独显 / 虚拟机无 GPU 都需支持
- **延迟要求**:不敏感(不追求极致低延迟)
- **并发模型**:通常 1 对 1少数 1 对多(每个连接独立编码器)
- **客户端**浏览器WebCodecs 优先,`<video>` 次之),未来集成
- **工具链**Visual Studio 2019
- **属性**:个人项目,暂不商用,专利问题搁置但仍优先选免专利方案
---
## 2. 技术方案总览
### 2.1 编码器优先级链
```
新连接进入(带客户端能力)
├─ 客户端声明支持 AV1─── 否 ────┐
│ 是 │
│ ↓ │
├─ av1_nvenc/qsv/amf 能开?──┐ │
│ │ │ │
│ 成功 → 用 AV1 │ │
│ ↓ ↓
└─ h264_nvenc/qsv/amf/mf 能开?──┐
│ │
成功 → 用 H.264 硬编 │
x264 软编(始终可用)
```
### 2.2 编码器后端表
| 类型 | FFmpeg 编码器名 | 硬件要求 | 备注 |
|---|---|---|---|
| AV1 硬编 | `av1_nvenc` | NVIDIA RTX 40+Ada Lovelace | 2022 Q4 起 |
| AV1 硬编 | `av1_amf` | AMD RX 7000+RDNA 3 | 2022 Q4 起 |
| AV1 硬编 | `av1_qsv` | Intel Arc / 部分新 Iris Xe | 2022 起 |
| H.264 硬编 | `h264_nvenc` | 几乎所有 NVIDIA GPUGTX 650+ | 2012 起 |
| H.264 硬编 | `h264_qsv` | 几乎所有 Intel 核显HD 4000+ | 2012 起 |
| H.264 硬编 | `h264_amf` | 几乎所有 AMD GPU | |
| H.264 硬编 | `h264_mf` | Windows Media Foundation | 兜底,质量/稳定性一般 |
| H.264 软编 | `libx264`(现有 `CX264Encoder` | 任意 CPU | 始终兜底 |
**不使用 `libx265` / `libaom-av1` / `libsvtav1`**CPU 软编),原因:
- 远控产品对 CPU 占用敏感AV1/HEVC 软编实时编码压力大
- `libx265` 会让 FFmpeg 切到 GPL`libaom-av1` 编码速度也不够
### 2.3 类结构
```
VideoEncoderBase新增抽象接口
├── CX264Encoder (改造现有类继承接口,保留软编兜底)
├── CFFmpegH264Encoder (新增,封装 h264_nvenc/qsv/amf/mf
└── CFFmpegAV1Encoder (新增,封装 av1_nvenc/qsv/amf
```
### 2.4 协商流程
```
握手阶段:
- 客户端(浏览器)在 WebSocket 握手时上报能力:
{ "codecs": ["av1", "h264"] } // 浏览器实际能解的,按优先级排
- 服务端取「客户端能力 ∩ 自己硬件能力」选 codec
会话阶段:
- 选定 codec 后创建对应编码器,整个连接生命周期不变
- 运行中不切换 codec保持简单需要切换就重连
```
---
## 3. 硬编 vs 现有 x264 软编对比
### 3.1 CPU 占用(最大收益)
| 编码器 | 1080p @ 30fps CPU 占用 |
|---|---|
| x264 `ultrafast`(现状) | 单核 1530% |
| x264 `medium`(同画质基准) | 单核 60100% |
| `h264_nvenc p4` | 总 **13%** |
| `h264_qsv medium` | 总 25% |
| `h264_amf balanced` | 总 25% |
被控端是用户的主力工作机他自己还在干活。CPU 让出来意味着远控对他几乎不可感。
### 3.2 同 CPU 预算下画质更高
x264 的 preset 排序(同码率下画质):
```
ultrafast < superfast < veryfast < faster < fast < medium < slow ...
↑ 现状 ↑ 标准基准
```
NVENC `p4` 预设大致对应 x264 `fast` ~ `medium`**画质明显优于当前 ultrafast且 CPU 占用低一个数量级**。
### 3.3 其他收益
- **编码延迟稳定**ASIC 不受 CPU 调度影响,单帧 15 ms
- **笔记本电池/温度**ASIC 几瓦,键盘不烫、风扇不转
- **可拉高分辨率/帧率**4K@30 / 多屏拼接软编扛不住,硬编轻松
### 3.4 代价(必须接受)
- **二进制 +610 MB**FFmpeg 静态库,可接受)
- **编译复杂度上升**vcpkg 或自编 FFmpeg
- **不同后端参数语义有差异**rc 模式、preset 名字、bitrate 表现都不一样
- **必须保留 x264 软编兜底**:无 GPU / 远程桌面 / 虚拟机 / NVENC session 满 等场景
---
## 4. 现有 H.264 编码器现状
`CX264Encoder` 签名(`client/X264Encoder.cpp`
```cpp
class CX264Encoder
{
private:
x264_t* m_pCodec;
x264_picture_t* m_pPicIn;
x264_picture_t* m_pPicOut;
x264_param_t m_Param;
public:
bool open(int width, int height, int fps, int crf);
bool open(x264_param_t* param);
void close();
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);
};
```
集成点(`client/ScreenCapture.h`
| 位置 | 内容 |
|---|---|
| `L148` | `CX264Encoder* m_encoder;`(持有具体类,需改为接口) |
| `L926-930` | 关键帧路径硬编码 `new CX264Encoder()` |
| `L956-960` | 增量帧路径硬编码 `new CX264Encoder()` |
| `L170-176` | `BitRateToCRF(bitRate)` 码率→CRF 映射 |
参数现状:
- `param.i_threads = 1`
- `preset = "ultrafast", tune = "zerolatency"`
- `i_keyint_max = fps * 15`15 秒一个 IDR
- `i_bframe = 0``b_open_gop = 0`
- `rc.i_rc_method = X264_RC_CRF`CRF 模式,未设 VBV
需要的改造(详见 §6
1. `ScreenCapture::m_encoder``std::unique_ptr<VideoEncoderBase>`
2. 编码器创建走 `CreateEncoder` 工厂
3. 接口的 quality 语义解耦CRF vs kbps详见 §6.2
---
## 5. FFmpeg 静态库准备
### 5.1 推荐方案vcpkg
VS2019 + vcpkg 是最稳的路径:
```
vcpkg install ffmpeg[core,nvcodec,amf,qsv]:x64-windows-static-md
```
要点:
- **三元组选 `x64-windows-static-md`**:链接静态 FFmpeg 但用动态 CRT`/MD`),与本项目当前工程一致
- 如果工程是 `/MT` 改用 `x64-windows-static`
- `nvcodec` feature 引入 NVENC 头文件,`amf` 引入 AMF`qsv` 引入 libmfx
- 阶段一只需要 H.264,可以先不带这些 feature但建议一次到位
阶段二需要 AV1FFmpeg 较新版本默认已支持 `av1_nvenc` / `av1_amf` / `av1_qsv`,无需额外 feature 名。
### 5.2 备选方案:自编
MSYS2 + MinGW-w64 + `--toolchain=msvc` 产出 MSVC 兼容 `.lib`
```bash
./configure \
--prefix=/path/to/install \
--arch=x86_64 \
--target-os=mingw64 \
--toolchain=msvc \
--disable-shared --enable-static \
--disable-everything \
--disable-autodetect \
--disable-network \
--disable-doc \
--disable-programs \
--disable-debug \
--enable-small \
--enable-encoder=h264_nvenc \
--enable-encoder=h264_amf \
--enable-encoder=h264_qsv \
--enable-encoder=h264_mf \
--enable-encoder=av1_nvenc \
--enable-encoder=av1_amf \
--enable-encoder=av1_qsv \
--enable-protocol=file
```
**不要** `--enable-gpl``--enable-libx265 / --enable-libaom`,保持 LGPL 且避免软编 H.265/AV1。
### 5.3 工程链接配置MSVC
**附加包含目录**C/C++ → 常规):
```
$(VcpkgRoot)\installed\x64-windows-static-md\include
```
**附加库目录**(链接器 → 常规):
```
$(VcpkgRoot)\installed\x64-windows-static-md\lib
```
**附加依赖项**(链接器 → 输入):
```
avcodec.lib
avutil.lib
swresample.lib
# FFmpeg 静态链接依赖的 Windows 系统库
mfplat.lib
mfuuid.lib
strmiids.lib
ws2_32.lib
secur32.lib
bcrypt.lib
```
**预处理器定义**
```
ENABLE_HW_ENCODER
__STDC_CONSTANT_MACROS # FFmpeg C 头文件在 C++ 中需要
```
---
## 6. 实现任务清单
### 6.1 文件清单
| 文件 | 操作 | 说明 |
|---|---|---|
| `VideoEncoderBase.h` | 新增 | 抽象基类 + EncoderParams |
| `X264Encoder.h/.cpp` | 修改 | 继承 `VideoEncoderBase`,新增 `forceIDR()` / `codec()` / `backendName()` |
| `CFFmpegH264Encoder.h/.cpp` | 新增(阶段一) | 封装 `h264_nvenc/qsv/amf/mf` |
| `CFFmpegAV1Encoder.h/.cpp` | 新增(阶段二) | 封装 `av1_nvenc/qsv/amf` |
| `EncoderFactory.h/.cpp` | 新增 | 工厂 + 多后端探测 |
| `EncoderProbe.h/.cpp` | 新增 | 启动时一次性探测可用后端,缓存结果 |
| `ScreenCapture.h` | 修改 | `m_encoder``std::unique_ptr<VideoEncoderBase>` |
| 握手协议代码 | 修改(阶段二) | 加 `codecs` 能力字段 |
| 工程配置 | 修改 | FFmpeg 静态库链接(详见 §5.3 |
### 6.2 抽象接口定义
`VideoEncoderBase.h`
```cpp
#pragma once
#include <cstdint>
enum class VideoCodec { H264, AV1 };
enum class RateControl { CRF, BITRATE };
struct EncoderParams {
int width;
int height;
int fps;
RateControl rc = RateControl::BITRATE;
int crf = 23; // 当 rc == CRF 时使用x264 路径)
int bitrate_kbps = 4000; // 当 rc == BITRATE 时使用(硬编路径)
int gop_seconds = 4; // 关键帧间隔(秒),编码器内部转 frames
};
class VideoEncoderBase {
public:
virtual ~VideoEncoderBase() = default;
virtual bool open(const EncoderParams& params) = 0;
virtual void close() = 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) {} // 默认空实现
virtual VideoCodec codec() const = 0;
virtual const char* backendName() const = 0; // "x264" / "h264_nvenc" / "av1_amf" ...
};
```
设计要点:
- **抛弃** `open(w, h, fps, quality)` 的设计 —— `quality` 在 H.264 是 CRF、在硬编是 kbps语义不清楚是坑
- 改用 `EncoderParams` 结构体 + `RateControl` 枚举,明确指定速率控制模式
- `backendName()` 返回实际后端名("x264" / "h264_nvenc" / "av1_amf"),调用方可用于日志/监控
### 6.3 CFFmpegH264Encoder 设计
```cpp
#pragma once
#include "VideoEncoderBase.h"
#include <vector>
#include <string>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
}
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;
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 convertToNV12(uint8_t* rgb, uint8_t bpp, 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; // RGB24 路径用
int64_t m_pts = 0;
bool m_forceIDR = false;
std::string m_backend;
};
```
#### 6.3.1 后端探测顺序
```cpp
static const char* kH264Backends[] = {
"h264_nvenc", // NVIDIA质量/速度/稳定性都最好
"h264_qsv", // Intel核显普及度高
"h264_amf", // AMD
"h264_mf", // Media Foundation 兜底(可选,质量一般)
};
bool CFFmpegH264Encoder::open(const EncoderParams& p) {
for (auto name : kH264Backends) {
if (tryOpenBackend(name, p)) {
m_backend = name;
return true;
}
cleanupCodec();
}
return false;
}
```
#### 6.3.2 各后端参数差异
```cpp
bool CFFmpegH264Encoder::tryOpenBackend(const char* name, const EncoderParams& p) {
const AVCodec* codec = avcodec_find_encoder_by_name(name);
if (!codec) return false;
m_ctx = avcodec_alloc_context3(codec);
if (!m_ctx) return false;
// 通用参数
m_ctx->width = p.width;
m_ctx->height = p.height;
m_ctx->time_base = {1, p.fps};
m_ctx->framerate = {p.fps, 1};
m_ctx->pix_fmt = AV_PIX_FMT_NV12;
m_ctx->gop_size = p.fps * p.gop_seconds;
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);
if (strcmp(name, "h264_nvenc") == 0) {
av_opt_set(m_ctx->priv_data, "preset", "p4", 0);
av_opt_set(m_ctx->priv_data, "tune", "ll", 0);
av_opt_set(m_ctx->priv_data, "rc", "cbr", 0);
av_opt_set(m_ctx->priv_data, "zerolatency", "1", 0);
av_opt_set(m_ctx->priv_data, "delay", "0", 0);
} else if (strcmp(name, "h264_qsv") == 0) {
av_opt_set(m_ctx->priv_data, "preset", "medium", 0);
av_opt_set_int(m_ctx->priv_data, "async_depth", 1, 0);
av_opt_set_int(m_ctx->priv_data, "low_power", 0, 0);
} else if (strcmp(name, "h264_amf") == 0) {
av_opt_set(m_ctx->priv_data, "usage", "lowlatency", 0);
av_opt_set(m_ctx->priv_data, "quality", "balanced", 0);
av_opt_set(m_ctx->priv_data, "rc", "cbr", 0);
} else if (strcmp(name, "h264_mf") == 0) {
av_opt_set_int(m_ctx->priv_data, "hw_encoding", 1, 0);
av_opt_set(m_ctx->priv_data, "rate_control", "cbr", 0);
}
if (avcodec_open2(m_ctx, codec, nullptr) < 0) return false;
m_frame = av_frame_alloc();
m_frame->format = AV_PIX_FMT_NV12;
m_frame->width = p.width;
m_frame->height = p.height;
if (av_frame_get_buffer(m_frame, 32) < 0) return false;
m_packet = av_packet_alloc();
return m_packet != nullptr;
}
```
#### 6.3.3 encode 实现
```cpp
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 (av_frame_make_writable(m_frame) < 0) return -1;
// 像素格式转换(直接用 libyuv与现有 x264 路径保持一致,不引入 sws_scale
int signed_height = direction * (int)height;
if (bpp == 32) {
libyuv::ARGBToNV12(
rgb, stride,
m_frame->data[0], m_frame->linesize[0],
m_frame->data[1], m_frame->linesize[1],
width, signed_height
);
} else if (bpp == 24) {
if (convertToNV12(rgb, bpp, 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_frame->key_frame = 1;
m_forceIDR = false;
} else {
m_frame->pict_type = AV_PICTURE_TYPE_NONE;
m_frame->key_frame = 0;
}
if (avcodec_send_frame(m_ctx, m_frame) < 0) return -3;
int ret = avcodec_receive_packet(m_ctx, m_packet);
if (ret == AVERROR(EAGAIN)) {
// 首帧延迟正常情况:返回成功但本次无输出
*lpSize = 0;
*lppData = nullptr;
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;
}
```
#### 6.3.4 RGB24 → NV12libyuv 无直接 API两步走
```cpp
int CFFmpegH264Encoder::convertToNV12(uint8_t* rgb, uint8_t /*bpp*/,
uint32_t stride, uint32_t width, uint32_t height,
int direction)
{
int signed_height = direction * (int)height;
int y_size = width * height;
int uv_size = (width / 2) * (height / 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;
libyuv::RGB24ToI420(
rgb, stride,
y, width,
u, width / 2,
v, width / 2,
width, signed_height
);
libyuv::I420ToNV12(
y, width,
u, width / 2,
v, width / 2,
m_frame->data[0], m_frame->linesize[0],
m_frame->data[1], m_frame->linesize[1],
width, height
);
return 0;
}
```
### 6.4 CFFmpegAV1Encoder 设计
结构与 `CFFmpegH264Encoder` **完全对称**,仅 backend 名换:
```cpp
static const char* kAV1Backends[] = {
"av1_nvenc", // RTX 40+
"av1_amf", // RX 7000+
"av1_qsv", // Intel Arc / 部分 11 代+ 核显
};
```
参数差异av1_nvenc 与 h264_nvenc 略有不同):
```cpp
if (strcmp(name, "av1_nvenc") == 0) {
av_opt_set(m_ctx->priv_data, "preset", "p4", 0);
av_opt_set(m_ctx->priv_data, "tune", "ll", 0);
av_opt_set(m_ctx->priv_data, "rc", "cbr", 0);
// AV1 特有tile-columns/rows 可调,多核解码更友好
av_opt_set_int(m_ctx->priv_data, "tile-columns", 1, 0);
} else if (strcmp(name, "av1_amf") == 0) {
av_opt_set(m_ctx->priv_data, "usage", "lowlatency", 0);
av_opt_set(m_ctx->priv_data, "quality", "balanced", 0);
av_opt_set(m_ctx->priv_data, "rc", "cbr", 0);
} else if (strcmp(name, "av1_qsv") == 0) {
av_opt_set(m_ctx->priv_data, "preset", "medium", 0);
av_opt_set_int(m_ctx->priv_data, "async_depth", 1, 0);
}
```
实现建议:先把 `CFFmpegH264Encoder` 跑通稳定,**再把它复制改名做 AV1 版本**。同步两个类的逻辑可以后续考虑抽公共基类,但不要为了 DRY 提前抽。
### 6.5 EncoderFactory 与探测
`EncoderFactory.h`
```cpp
#pragma once
#include "VideoEncoderBase.h"
#include <memory>
struct ClientCapability {
bool supportAV1 = false;
bool supportH264 = true; // 假定都支持
};
struct EncoderRequest {
int width, height, fps;
int bitrate_kbps;
ClientCapability client;
};
std::unique_ptr<VideoEncoderBase> CreateEncoder(const EncoderRequest& req);
```
`EncoderFactory.cpp`
```cpp
#include "EncoderFactory.h"
#include "EncoderProbe.h"
#include "CFFmpegAV1Encoder.h"
#include "CFFmpegH264Encoder.h"
#include "X264Encoder.h"
std::unique_ptr<VideoEncoderBase> CreateEncoder(const EncoderRequest& req) {
EncoderParams p;
p.width = req.width;
p.height = req.height;
p.fps = req.fps;
// 1. AV1 路径(仅当客户端支持且启动探测确认硬件可用)
if (req.client.supportAV1 && EncoderProbe::HasAV1Hw()) {
auto enc = std::make_unique<CFFmpegAV1Encoder>();
p.rc = RateControl::BITRATE;
p.bitrate_kbps = req.bitrate_kbps;
if (enc->open(p)) {
LOG_INFO("encoder: AV1 backend=%s", enc->backendName());
return enc;
}
LOG_WARN("encoder: AV1 open failed, falling back to H.264");
}
// 2. H.264 硬编路径
if (EncoderProbe::HasH264Hw()) {
auto enc = std::make_unique<CFFmpegH264Encoder>();
p.rc = RateControl::BITRATE;
p.bitrate_kbps = req.bitrate_kbps;
if (enc->open(p)) {
LOG_INFO("encoder: H264-HW backend=%s", enc->backendName());
return enc;
}
LOG_WARN("encoder: H264 HW open failed, falling back to x264");
}
// 3. H.264 软编兜底(始终可用)
{
auto enc = std::make_unique<CX264Encoder>();
p.rc = RateControl::CRF;
p.crf = BitRateToCRF(req.bitrate_kbps);
if (enc->open(p)) {
LOG_INFO("encoder: H264-SW (libx264)");
return enc;
}
}
LOG_ERROR("encoder: all backends failed");
return nullptr;
}
```
要点:
- **不要**引入会话池(`HEVCSessionPool` 那套)—— 个人项目场景下不需要
- NVENC session 限制由 FFmpeg 自己报错,工厂捕获后自动降级
- 失败路径都打日志,方便定位
### 6.6 启动时一次性后端探测
避免每个新连接都重复尝试每个 backend
```cpp
// EncoderProbe.h
class EncoderProbe {
public:
static void RunOnce(); // 程序启动时调用一次
static bool HasAV1Hw();
static bool HasH264Hw();
static const char* PreferredAV1Backend(); // 第一个能用的 AV1 后端名
static const char* PreferredH264Backend();
};
// EncoderProbe.cpp 实现思路:
// 对每个候选后端尝试 alloc_context → open2 → free
// 用最低分辨率(如 640x480 @30fps减少探测开销
// 结果缓存在静态变量,加 std::once_flag 保证线程安全
```
启动时探测 1 次,运行时 `CreateEncoder` 直接读结果。
### 6.7 ScreenCapture.h 改造
**当前**`client/ScreenCapture.h:148, 926-930, 956-960`
```cpp
CX264Encoder* m_encoder;
// ...
m_encoder = new CX264Encoder();
int br = (m_nBitRate > 0) ? m_nBitRate : (width * height / 1266);
m_encoder->open(width, height, 20, BitRateToCRF(br));
```
**改造后**
```cpp
std::unique_ptr<VideoEncoderBase> m_encoder;
ClientCapability m_clientCap; // 握手阶段填入
// ...
if (!m_encoder) {
EncoderRequest req{
(int)width, (int)height, 20,
m_nBitRate > 0 ? m_nBitRate : (int)(width * height / 1266),
m_clientCap
};
m_encoder = CreateEncoder(req);
if (!m_encoder) return nullptr;
}
int err = m_encoder->encode(nextData, 32, 4 * width, width, height,
&encoded_data, &encoded_size);
```
注意:两处 `new CX264Encoder()` 提取成一个 `ensureEncoder()` 私有方法,避免重复。
### 6.8 协议握手(阶段二)
客户端浏览器 WebSocket 连接时上报:
```json
{
"type": "client_capability",
"codecs": ["av1", "h264"]
}
```
浏览器端探测脚本JS
```javascript
async function probeBrowserCodecs() {
const codecs = [];
if (typeof VideoDecoder !== 'undefined') {
// AV1 Main Profile, Level 4.0, 8-bit
const av1 = await VideoDecoder.isConfigSupported({ codec: 'av01.0.04M.08' });
if (av1.supported) codecs.push('av1');
}
codecs.push('h264'); // 兜底假定支持,<video> 标签也支持
return codecs;
}
```
**向后兼容**:老版本客户端不发 `codecs` 字段 → 服务端按 H.264 处理。`ClientCapability::supportAV1` 默认 false。
可参考 `docs/hevc_browser_decode_test.html`(改造一份 AV1 版本)做浏览器解码端到端验证。
---
## 7. 像素格式与转换
保持与现有 x264 路径完全一致的做法:
- **硬编内部格式**`AV_PIX_FMT_NV12`NVENC/QSV/AMF 通用)
- **转换库**libyuv不引入 `sws_scale`
- **BGRA → NV12**`libyuv::ARGBToNV12` 直接
- **RGB24 → NV12**libyuv 无直接 API分两步 RGB24 → I420 → NV12
- **direction 参数**:沿用现有 `X264Encoder.cpp:136` 的写法(`direction * height` 作为乘子)
---
## 8. 测试要求
### 8.1 单元测试
- `VideoEncoderBase` 各实现的 `open / encode / close` 生命周期
- `CFFmpegH264Encoder` 在缺失各后端时降级到下一个
- `EncoderFactory` 在不同 `ClientCapability` × 硬件能力 矩阵下返回正确后端
- `EncoderProbe::RunOnce()` 在不同 GPU 上的结果一致性
### 8.2 集成测试
| 场景 | 期望 |
|---|---|
| 客户端支持 AV1 + 编码端 RTX 40 | 使用 AV1`av1_nvenc` |
| 客户端支持 AV1 + 编码端 GTX 1080 | 降级 H.264 硬编(`h264_nvenc` |
| 客户端不支持 AV1 + 编码端任意 | H.264(硬编优先) |
| 编码端无 GPU / 虚拟机 | x264 软编 |
| 编码端集显 + 独显 | 优先级中第一个成功的后端 |
| 中途客户端能力变化 | 当前连接不变;下次握手按新能力 |
| H264 硬编创建失败 | 自动回落 x264 软编,连接不断 |
### 8.3 硬件验证矩阵
| 编码端 | 客户端浏览器 | 期望 |
|---|---|---|
| RTX 40 / 50 | Chrome / Firefox / Edge | AV1 |
| GTX 10/16 / RTX 20/30 | Chrome / Firefox / Edge | H.264 NVENC |
| Intel Arc | Chrome / Firefox | AV1`av1_qsv` |
| Intel 12 代+ 核显 | Chrome / Firefox | H.264 QSV |
| AMD RX 7000+ | Chrome / Firefox | AV1`av1_amf` |
| AMD 老卡 | Chrome / Firefox | H.264 AMF |
| 虚拟机 / 远程桌面会话 | 任意 | x264 软编 |
| iOS Safari < 17 | 任意编码端 | H.264 |
| iOS Safari ≥ 17A17 Pro+ | 任意编码端 | AV1 优先 |
### 8.4 体积验证
- exe 体积增量 < 12 MBvcpkg 静态链接,含 AV1+H264 全后端)
- 若超出明显,检查 vcpkg feature 是否引入了不需要的 codec
### 8.5 回归测试(关键)
每一步改造后必须验证:
- 现有 x264 软编通路完全可用(在禁用所有硬编后端的环境下)
- 现有客户端(不发 `codecs` 字段)可正常工作
- 编码码流向后兼容,老客户端能解
---
## 9. 已知风险与注意事项
### 9.1 多 GPU 跨适配器
笔记本集显+独显场景FFmpeg 默认走主显卡。可能报 "failed to create device"。**Catch 后回落到下一个后端**,不要直接终止。
### 9.2 第一帧延迟
FFmpeg 硬编可能在首次 `send_frame``receive_packet` 返回 `EAGAIN`。调用方代码必须能处理 `*lpSize == 0` 的情况(返回 0 表示成功但本次无输出)。`ScreenCapture::GetNextScreenData` 当前 `encoded_size == 0` 会怎么处理需要确认。
### 9.3 NVENC session 数限制
NVIDIA **消费级**卡GeForce有 NVENC session 上限(驱动 522.25+ 起 3 个,更新版可能放宽到 5。多连接超限时 `open` 失败 → 工厂自动降级到 QSV/AMF/x264。**不要做"会话池"提前拦截** —— 让 FFmpeg 自己报错,工厂处理。
### 9.4 浏览器解 AV1 的硬件依赖
- 桌面 Chrome/Firefox/Edge软解兜底CPU 占用高,但能用)
- 移动端iPhone 15 Pro+ / M3+ 才有 AV1 硬解,老设备只能软解(可能卡)
- `isConfigSupported` 报 true 不等于跑得流畅。**建议握手时也带上设备类型**,弱设备强制走 H.264
### 9.5 LGPL 静态链接合规
个人项目暂搁置。商用前需法务确认FFmpeg 源代码提供、重新链接能力等)。
### 9.6 配置项(建议)
建议做成 INI/JSON
```
encoder.prefer_av1 = true
encoder.h264.bitrate_default = 4000 (kbps)
encoder.fallback_to_x264 = true
encoder.probe_at_startup = true
encoder.disable_h264_mf = true (h264_mf 质量一般,可禁用)
```
### 9.7 日志要求
关键路径必须有日志:
- 启动探测结果:每个后端是否可用 + 失败原因(`av_err2str`
- 每个连接选定的 `codec` + `backend`INFO
- 后端打开失败 + 回落WARN
- 编码过程中的异常ERROR
### 9.8 线程安全
- 每个编码器实例不跨线程
- `EncoderProbe` 单例首次初始化加 `std::once_flag`
- FFmpeg 新版本不需要全局初始化(`av_register_all` 已废弃)
### 9.9 与现有 `DISABLE_X264_FOR_TEST` 编译开关协同
项目已有 `DISABLE_X264_FOR_TEST` 宏(见 `X264Encoder.cpp:5`)和最近 `c0a632a` 提交"Compliance: Add building option to disable x264 and ffmpeg"。新代码须遵循同样的可禁用约定:
- `ENABLE_HW_ENCODER` 关闭时整个 `CFFmpegH264Encoder` / `CFFmpegAV1Encoder` 编译为空实现或不参与链接
- 工厂在该宏关闭时直接返回 `CX264Encoder`
---
## 10. 实现顺序建议
**每一步独立可合入**,每一步完成后 x264 通路必须可用、客户端无感知。
### Step 0抽象层零功能改动
1. 新建 `VideoEncoderBase.h`,定义接口 + `EncoderParams` + `RateControl`
2. `CX264Encoder` 改造继承 `VideoEncoderBase`
- 新增 `forceIDR()`(设置一个标志,下次 encode 时通过 `x264_picture_t::i_type = X264_TYPE_IDR`
- 实现 `codec()` 返回 `H264`
- 实现 `backendName()` 返回 `"x264"`
-`open(w, h, fps, crf)` 签名保留,转调新的 `open(EncoderParams)`
3. `ScreenCapture::m_encoder``std::unique_ptr<VideoEncoderBase>`,但仍直接 `new CX264Encoder`
4. **不引 FFmpeg、不引工厂**
5. 验证H.264 通路完全不变,对外行为零变化
### Step 1FFmpeg 集成 + `h264_nvenc` 单后端
1. vcpkg 安装 `ffmpeg[core,nvcodec]:x64-windows-static-md`
2. 工程添加包含目录 / 库目录 / 系统库
3. 新建 `CFFmpegH264Encoder`,仅实现 `h264_nvenc`
4.`ScreenCapture` 加临时开关:硬编码切到 `CFFmpegH264Encoder` 跑一下
5. 用浏览器解码 demo 验证码流能解
6. 体积验证(应 +46 MB
### Step 2扩展 H.264 硬编后端
1. `CFFmpegH264Encoder``h264_qsv` / `h264_amf` 探测
2. 顺序:`nvenc → qsv → amf``mf` 可暂不接)
3. 不同后端的参数适配(见 §6.3.2
4. 测试 Intel 核显 + AMD 卡
### Step 3工厂 + 软编兜底
1. 新建 `EncoderFactory` / `EncoderProbe`
2. 工厂按 `H264 硬编 → x264 软编` 顺序
3. `ScreenCapture` 改用工厂(消除两处 `new CX264Encoder`
4. 测试无 GPU 环境降级到 x264
### Step 4AV1 路径(独立闭环)
1. 重跑 vcpkg`ffmpeg[core,nvcodec,amf,qsv]:x64-windows-static-md`
2. 新建 `CFFmpegAV1Encoder`,结构与 H.264 对称(直接 copy + 改 backend 名 + 调参)
3. `EncoderProbe` 加 AV1 探测
4. 工厂前置 AV1 路径
5. 硬件验证矩阵执行
### Step 5握手协商 + 浏览器探测
1. 客户端 JS `isConfigSupported` 探测 AV1 / H.264
2. WebSocket 握手字段 `codecs` 上报
3. 服务端解析后填入 `ClientCapability`
4. 老客户端向后兼容(无 `codecs` 字段 → 默认 H.264
5. 端到端验证:编码端 RTX 40 + 浏览器 Chrome 走 AV1回落场景走 H.264
---
## 11. 不在本次范围
- HEVC 编码(决策已排除,见 §1.3.1
- 软编 AV1libaom / SVT-AV1CPU 占用不适合远控)
- 运行中动态切换 codec需要切换就重连
- 转发流1 路编码多路分发)
- 桌面捕获共享
- 客户端浏览器解码具体实现(可参考 `docs/hevc_browser_decode_test.html` 改 AV1 版本验证)
- Linux/macOS 移植
- FFmpeg DLL 形式分发
---
## 12. 参考资料
- FFmpeg 编码器列表:`ffmpeg -encoders | grep -E "h264|av1"`
- NVENC 参数:`ffmpeg -h encoder=h264_nvenc``ffmpeg -h encoder=av1_nvenc`
- QSV 参数:`ffmpeg -h encoder=h264_qsv``ffmpeg -h encoder=av1_qsv`
- AMF 参数:`ffmpeg -h encoder=h264_amf``ffmpeg -h encoder=av1_amf`
- NVIDIA Video Codec Support Matrixhttps://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
- WebCodecs APIhttps://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API
- libyuvhttps://chromium.googlesource.com/libyuv/libyuv/
- vcpkg ffmpeg porthttps://github.com/microsoft/vcpkg/tree/master/ports/ffmpeg
- FFmpeg HWAccel Introhttps://trac.ffmpeg.org/wiki/HWAccelIntro
- AOMedia AV1https://aomedia.org/
---
**文档结束**
实现时如遇到本文档未覆盖的设计抉择,优先选择**简单、与现有 x264 通路对称、不破坏已有功能、不增加运行时外部依赖**的方案,并在代码注释中说明决策依据。