在上一章 捕获-预处理-编码流水线] 中,我们了解了整个屏幕录制过程就像一条工厂流水线。数据从捕获开始,经过预处理,最后被编码成视频。
那么,问题来了:谁是这条流水线的"总管"呢?谁来确保每个环节都按时、按顺序地工作?
答案就是我们本章的主角:DemoApplication
类。
什么是 DemoApplication
?
DemoApplication
是整个项目的"大脑"和"总指挥"。它是一个 C++ 类,专门被设计用来封装和管理流水线中的所有核心组件。它负责:
- 创建和初始化:在程序开始时,它会创建捕获器、预处理器和编码器这三个核心组件,并确保它们都已准备就绪。
- 驱动流水线:在录制过程中,它在一个循环里发号施令,让每个组件依次完成自己的工作:捕获一帧、处理该帧、编码该帧。
- 资源清理:在程序结束时,它负责安全地释放所有组件占用的资源,避免内存泄漏。
我们可以再次使用上一章的电影制作 比喻来理解它。如果说 DDAImpl
是摄影师,RGBToNV12
是后期调色师,NvEncoderD3D11
是剪辑师,那么 DemoApplication
就是 电影导演。
这位导演的工作就是:
- 前期筹备 (
Init
):招募并组织好整个剧组(摄影、调色、剪辑团队)。 - 开机拍摄 (循环执行) :对每一幕(每一帧画面)下达指令:"摄影师,开拍!" (
Capture
),"调色师,处理一下素材!" (Preproc
),"剪辑师,把这段加到电影里!" (Encode
)。 - 杀青散场 (
Cleanup
):电影拍完后,解散剧组,收拾片场。
通过将所有复杂的协调工作都封装在 DemoApplication
内部,我们的主程序 (main.cpp
里的 Grab60FPS
函数) 逻辑变得异常清晰和简单。
DemoApplication
如何简化工作流程
让我们看看 main.cpp
中的 Grab60FPS
函数是如何使用 DemoApplication
的。代码被大大简化,只保留了核心骨架:
cpp
// Demo 60 FPS (approx.) capture
int Grab60FPS(int nFrames)
{
// 1. 创建我们的"导演"
DemoApplication Demo;
// 2. 导演进行前期筹备,初始化所有组件
HRESULT hr = Demo.Init();
if (FAILED(hr))
{
// 如果初始化失败,就直接退出
return -1;
}
// 3. 开始循环拍摄每一帧
do
{
// "导演"指挥流水线工作
hr = Demo.Capture(wait); // 捕获
hr = Demo.Preproc(); // 预处理
hr = Demo.Encode(); // 编码
capturedFrames++;
} while (capturedFrames <= nFrames);
return 0; // 拍摄完成,程序结束
}
看到了吗?主逻辑就是简单地创建一个 Demo
对象,调用 Init()
,然后在循环里调用 Capture()
、Preproc()
和 Encode()
。所有复杂的细节都被隐藏在了 DemoApplication
类的内部。这就是封装带来的巨大好处!
深入内部:DemoApplication
的三大职责
现在,让我们揭开导演的神秘面纱,看看它的内部是如何运作的。DemoApplication
的工作可以分为三个主要阶段:初始化、执行循环和清理。
1. 初始化阶段 (Init
方法)
当你调用 Demo.Init()
时,"导演"开始了他的前期筹备工作。他需要确保所有的"工作人员"------也就是流水线上的各个组件------都已就位。
Init()
方法会按顺序调用一系列私有的初始化函数:
cpp
// 文件: main.cpp (DemoApplication::Init)
HRESULT Init()
{
HRESULT hr = S_OK;
// 步骤 1: 初始化 DirectX 环境,这是所有图形操作的基础
hr = InitDXGI();
returnIfError(hr);
// 步骤 2: 初始化桌面捕获器
hr = InitDup();
returnIfError(hr);
// 步骤 3: 初始化 NVENC 编码器
hr = InitEnc();
returnIfError(hr);
// 步骤 4: 初始化色彩转换器
hr = InitColorConv();
returnIfError(hr);
// ... 其他初始化 ...
return hr;
}
这个过程就像一个清单,DemoApplication
会逐一确认:
InitDXGI()
: 搭建好"片场"(DirectX 11 环境)。InitDup()
: 确保"摄影师" 桌面复制接口 (DDAImpl) 已准备就绪。InitEnc()
: 确保"剪辑师" NVENC 硬件编码器封装 (NvEncoderD3D11) 已准备就绪。InitColorConv()
: 确保"调色师" 色彩空间转换器 (RGBToNV12) 已准备就绪。
我们可以用一个时序图来更清晰地展示这个过程:
只有当所有组件都报告"准备就绪"后,Init()
才会成功返回,录制工作才能正式开始。
2. 执行循环 (Capture, Preproc, Encode)
初始化完成后,程序进入 do-while
循环,开始一帧一帧地处理画面。DemoApplication
在这里扮演着数据传递和流程控制的核心角色。
数据是如何在组件间流动的?
DemoApplication
内部维护着几个重要的"中转站",它们是 DirectX 纹理(可以理解为显存中的一块图像区域):
pDupTex2D
: 用于存放从屏幕上捕获的原始 RGBA 图像。pEncBuf
: 用于存放经过色彩转换后的NV12 格式图像。
现在我们来看看三步曲是如何协作的:
第一步: Capture()
cpp
// 文件: main.cpp (DemoApplication::Capture)
HRESULT Capture(int wait)
{
// 调用 DDAImpl 组件,将捕获到的帧存入 pDupTex2D
HRESULT hr = pDDAWrapper->GetCapturedFrame(&pDupTex2D, wait);
// ... 错误处理 ...
return hr;
}
导演喊"开拍!",pDDAWrapper
(桌面复制接口 (DDAImpl) 对象)立即工作,它将屏幕画面捕获到 pDupTex2D
这个纹理中。现在,我们有了一帧原始的 RGBA 格式的画面。
第二步: Preproc()
cpp
// 文件: main.cpp (DemoApplication::Preproc)
HRESULT Preproc()
{
// 从编码器获取一个用于存放 NV12 图像的空纹理
const NvEncInputFrame *pEncInput = pEnc->GetNextInputFrame();
pEncBuf = (ID3D11Texture2D *)pEncInput->inputPtr;
// 调用色彩转换器,将 pDupTex2D 的内容转成 NV12 并存入 pEncBuf
hr = pColorConv->Convert(pDupTex2D, pEncBuf);
// 原始图像已经没用了,释放它
SAFE_RELEASE(pDupTex2D);
return hr;
}
导演把原始素材(pDupTex2D
)交给调色师 pColorConv
(色彩空间转换器 (RGBToNV12) 对象)。调色师将其转换为编码器最喜欢的 NV12 格式,并把结果存放在 pEncBuf
中。
第三步: Encode()
cpp
// 文件: main.cpp (DemoApplication::Encode)
HRESULT Encode()
{
// ...
// 将预处理好的 NV12 图像 (pEncBuf) 交给编码器
pEnc->EncodeFrame(vPacket);
// 将编码后的数据写入文件
WriteEncOutput();
// ...
// NV12 图像也用完了,释放它
SAFE_RELEASE(pEncBuf);
return hr;
}
最后,导演将处理好的 NV12 素材(pEncBuf
)交给剪辑师 pEnc
(NVENC 硬件编码器封装 (NvEncoderD3D11) 对象)。剪辑师利用 GPU 硬件加速,将其压缩成 H.264 视频数据,然后 DemoApplication
负责将这些数据写入到最终的视频文件中。
至此,一帧画面的完整处理流程就结束了。这个循环会不断重复,直到录制到指定数量的帧。
3. 清理阶段 (Cleanup
和析构函数)
当录制结束,DemoApplication
对象生命周期结束时(在 Grab60FPS
函数末尾),它的析构函数 ~DemoApplication()
会被自动调用。
cpp
// 文件: main.cpp (DemoApplication::~DemoApplication)
~DemoApplication()
{
// 调用 Cleanup 方法来释放所有资源
Cleanup(true);
}
析构函数会调用 Cleanup()
方法,这是一个专门负责"打扫战场"的函数。它会按照与初始化相反的顺序,安全地释放所有组件和资源。
cpp
// 文件: main.cpp (DemoApplication::Cleanup)
void Cleanup(bool bDelete = true)
{
// 释放 DDA 组件
if (pDDAWrapper)
{
pDDAWrapper->Cleanup();
delete pDDAWrapper;
pDDAWrapper = nullptr;
}
// 释放编码器,并确保所有缓冲的帧都被写入文件
if (pEnc)
{
pEnc->EndEncode(vPacket);
WriteEncOutput();
pEnc->DestroyEncoder();
delete pEnc;
pEnc = nullptr;
}
// ... 释放其他资源,如色彩转换器、DXGI 设备等 ...
}
这确保了程序退出时不会有任何资源泄漏,就像电影拍完后,所有工作人员都能拿到工资,所有设备都归还妥当一样。
总结
在本章中,我们认识了 nvEncDXGI
项目的总指挥------DemoApplication
类。
- 它是"导演" :它不亲自执行具体任务,而是负责协调和管理手下的专业团队(
DDAImpl
,RGBToNV12
,NvEncoderD3D11
)。 - 它封装了复杂性:通过将初始化、执行循环和资源清理的逻辑都封装起来,它让主程序的代码变得极其简洁。
- 它定义了清晰的流程 :通过
Init
->Capture
->Preproc
->Encode
->Cleanup
这样一个生命周期,它确保了整个流水线有序、高效地运作。