Files
SimpleRemoter/docs/HardwareEncoding_Design.md
yuanyuanxiang 8c7f612449 Feature: Implement H.264 and AV1 hardware encoding for remote control
Remark: Need to update FFmpeg static libraries to take effort
2026-05-30 00:12:38 +02:00

33 KiB
Raw Permalink Blame History

视频编码硬件加速实现指导文档

本文档供 AI 编码助手参考,用于在现有 C++ 远程控制程序中实现 H.264 硬件编码 + AV1 编码路径。


1. 项目背景

1.1 当前状态

  • C++ Windows 远程控制程序
  • 已实现 H.264 编码,基于 x264 软编(CX264Encoderpreset = 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 / libsvtav1CPU 软编),原因:

  • 远控产品对 CPU 占用敏感AV1/HEVC 软编实时编码压力大
  • libx265 会让 FFmpeg 切到 GPLlibaom-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 MBFFmpeg 静态库,可接受)
  • 编译复杂度上升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 = 1
  • preset = "ultrafast", tune = "zerolatency"
  • i_keyint_max = fps * 1515 秒一个 IDR
  • i_bframe = 0b_open_gop = 0
  • rc.i_rc_method = X264_RC_CRFCRF 模式,未设 VBV

需要的改造(详见 §6

  1. ScreenCapture::m_encoderstd::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 引入 AMFqsv 引入 libmfx
  • 阶段一只需要 H.264,可以先不带这些 feature但建议一次到位

阶段二需要 AV1FFmpeg 较新版本默认已支持 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_encoderstd::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 → NV12libyuv 无直接 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_NV12NVENC/QSV/AMF 通用)
  • 转换库libyuv不引入 sws_scale
  • BGRA → NV12libyuv::ARGBToNV12 直接
  • RGB24 → NV12libyuv 无直接 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 使用 AV1av1_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 AV1av1_qsv
Intel 12 代+ 核显 Chrome / Firefox H.264 QSV
AMD RX 7000+ Chrome / Firefox AV1av1_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_framereceive_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 + backendINFO
  • 后端打开失败 + 回落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_encoderstd::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. CFFmpegH264Encoderh264_qsv / h264_amf 探测
  2. 顺序:nvenc → qsv → amfmf 可暂不接)
  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. 重跑 vcpkgffmpeg[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. 参考资料


文档结束

实现时如遇到本文档未覆盖的设计抉择,优先选择简单、与现有 x264 通路对称、不破坏已有功能、不增加运行时外部依赖的方案,并在代码注释中说明决策依据。