33 KiB
视频编码硬件加速实现指导文档
本文档供 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 优先,
<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 GPU(GTX 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(现状) |
单核 15–30% |
x264 medium(同画质基准) |
单核 60–100% |
h264_nvenc p4 |
总 1–3% |
h264_qsv medium |
总 2–5% |
h264_amf balanced |
总 2–5% |
被控端是用户的主力工作机,他自己还在干活。CPU 让出来意味着远控对他几乎不可感。
3.2 同 CPU 预算下画质更高
x264 的 preset 排序(同码率下画质):
ultrafast < superfast < veryfast < faster < fast < medium < slow ...
↑ 现状 ↑ 标准基准
NVENC p4 预设大致对应 x264 fast ~ medium,画质明显优于当前 ultrafast,且 CPU 占用低一个数量级。
3.3 其他收益
- 编码延迟稳定:ASIC 不受 CPU 调度影响,单帧 1–5 ms
- 笔记本电池/温度:ASIC 几瓦,键盘不烫、风扇不转
- 可拉高分辨率/帧率:4K@30 / 多屏拼接软编扛不住,硬编轻松
3.4 代价(必须接受)
- 二进制 +6–10 MB(FFmpeg 静态库,可接受)
- 编译复杂度上升:vcpkg 或自编 FFmpeg
- 不同后端参数语义有差异:rc 模式、preset 名字、bitrate 表现都不一样
- 必须保留 x264 软编兜底:无 GPU / 远程桌面 / 虚拟机 / NVENC session 满 等场景
4. 现有 H.264 编码器现状
CX264Encoder 签名(client/X264Encoder.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 = 1preset = "ultrafast", tune = "zerolatency"i_keyint_max = fps * 15(15 秒一个 IDR)i_bframe = 0、b_open_gop = 0rc.i_rc_method = X264_RC_CRF(CRF 模式,未设 VBV)
需要的改造(详见 §6):
ScreenCapture::m_encoder改std::unique_ptr<VideoEncoderBase>- 编码器创建走
CreateEncoder工厂 - 接口的 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 nvcodecfeature 引入 NVENC 头文件,amf引入 AMF,qsv引入 libmfx- 阶段一只需要 H.264,可以先不带这些 feature,但建议一次到位
阶段二需要 AV1,FFmpeg 较新版本默认已支持 av1_nvenc / av1_amf / av1_qsv,无需额外 feature 名。
5.2 备选方案:自编
MSYS2 + MinGW-w64 + --toolchain=msvc 产出 MSVC 兼容 .lib:
./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:
#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 设计
#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 后端探测顺序
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 各后端参数差异
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 实现
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 → NV12(libyuv 无直接 API,两步走)
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 名换:
static const char* kAV1Backends[] = {
"av1_nvenc", // RTX 40+
"av1_amf", // RX 7000+
"av1_qsv", // Intel Arc / 部分 11 代+ 核显
};
参数差异(av1_nvenc 与 h264_nvenc 略有不同):
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:
#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:
#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:
// 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):
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));
改造后:
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 连接时上报:
{
"type": "client_capability",
"codecs": ["av1", "h264"]
}
浏览器端探测脚本(JS):
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 ≥ 17(A17 Pro+) | 任意编码端 | AV1 优先 |
8.4 体积验证
- exe 体积增量 < 12 MB(vcpkg 静态链接,含 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:抽象层(零功能改动)
- 新建
VideoEncoderBase.h,定义接口 +EncoderParams+RateControl CX264Encoder改造继承VideoEncoderBase:- 新增
forceIDR()(设置一个标志,下次 encode 时通过x264_picture_t::i_type = X264_TYPE_IDR) - 实现
codec()返回H264 - 实现
backendName()返回"x264" - 旧
open(w, h, fps, crf)签名保留,转调新的open(EncoderParams)
- 新增
ScreenCapture::m_encoder改std::unique_ptr<VideoEncoderBase>,但仍直接new CX264Encoder- 不引 FFmpeg、不引工厂
- 验证:H.264 通路完全不变,对外行为零变化
Step 1:FFmpeg 集成 + h264_nvenc 单后端
- vcpkg 安装
ffmpeg[core,nvcodec]:x64-windows-static-md - 工程添加包含目录 / 库目录 / 系统库
- 新建
CFFmpegH264Encoder,仅实现h264_nvenc - 在
ScreenCapture加临时开关:硬编码切到CFFmpegH264Encoder跑一下 - 用浏览器解码 demo 验证码流能解
- 体积验证(应 +4–6 MB)
Step 2:扩展 H.264 硬编后端
CFFmpegH264Encoder加h264_qsv/h264_amf探测- 顺序:
nvenc → qsv → amf(mf可暂不接) - 不同后端的参数适配(见 §6.3.2)
- 测试 Intel 核显 + AMD 卡
Step 3:工厂 + 软编兜底
- 新建
EncoderFactory/EncoderProbe - 工厂按
H264 硬编 → x264 软编顺序 ScreenCapture改用工厂(消除两处new CX264Encoder)- 测试无 GPU 环境降级到 x264
Step 4:AV1 路径(独立闭环)
- 重跑 vcpkg:
ffmpeg[core,nvcodec,amf,qsv]:x64-windows-static-md - 新建
CFFmpegAV1Encoder,结构与 H.264 对称(直接 copy + 改 backend 名 + 调参) EncoderProbe加 AV1 探测- 工厂前置 AV1 路径
- 硬件验证矩阵执行
Step 5:握手协商 + 浏览器探测
- 客户端 JS
isConfigSupported探测 AV1 / H.264 - WebSocket 握手字段
codecs上报 - 服务端解析后填入
ClientCapability - 老客户端向后兼容(无
codecs字段 → 默认 H.264) - 端到端验证:编码端 RTX 40 + 浏览器 Chrome 走 AV1,回落场景走 H.264
11. 不在本次范围
- HEVC 编码(决策已排除,见 §1.3.1)
- 软编 AV1(libaom / SVT-AV1,CPU 占用不适合远控)
- 运行中动态切换 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 Matrix:https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
- WebCodecs API:https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API
- libyuv:https://chromium.googlesource.com/libyuv/libyuv/
- vcpkg ffmpeg port:https://github.com/microsoft/vcpkg/tree/master/ports/ffmpeg
- FFmpeg HWAccel Intro:https://trac.ffmpeg.org/wiki/HWAccelIntro
- AOMedia AV1:https://aomedia.org/
文档结束
实现时如遇到本文档未覆盖的设计抉择,优先选择简单、与现有 x264 通路对称、不破坏已有功能、不增加运行时外部依赖的方案,并在代码注释中说明决策依据。