☰
Windows录屏软件优化实战:帧采集、音画同步与编码避坑指南
2026/10/13 13:14:42 网站建设 项目流程

简介:面向音视频编程入门与进阶学习者,这是一套完整的录屏软件开发工程,基于 Qt 4.8.4 与 FFmpeg 2.5.2 构建,演示了从初始化到最终完善的开发流程。压缩包共 289 个文件、17.43MB,包含 226 个头文件、14 个 lib 库、12 个 dll 运行库、12 个 a 链接库,以及 cpp 源码、ui 界面和 pro 工程配置,整体结构清晰。已有 1075 人学习,结合作者提供的图文讲解,可深入理解录屏核心原理、FFmpeg 接口封装及 Qt 界面设计。运行依赖 ffmpeg/bin 下的动态库,可直接在此基础上扩展区域录制、音视频推流或定时录屏功能。

1. 录屏软件的“最终完善”到底在完善什么:从能录到好用

一直跟到这一讲的同学应该都有同感:录屏软件做到“能录”,通常一周就够,但做到“好用”,可能要再花三四周。这中间差的不是新功能,而是稳定性、同步精度和资源占用这些看不见的细节。录了十几分钟音画对不上、切换分辨率后黑屏、内存在录到一半时悄悄涨满,这些才是“最终完善”要解决的问题。这一讲我会按帧采集、音画同步、编码封装、踩坑排查四条线,把一套能从 demo 走向真实交付的录屏方案完整过一遍,新手可以逐段照做,熟手可以直接跳到第 5、6 章对照自己的实现。

2. 帧采集与编码链路:先把录制主轴立住

录屏软件的性能问题里,有一半以上出在“画面怎么取出来”和“数据怎么压进去”这两条主线上。这一章把选型定下来,后续完善才有地基。

2.1 帧采集选型:GDI、DXGI 与 Graphics Capture 怎么选

帧采集就是每隔一个固定间隔,把屏幕上的像素拷贝到我们可以编码的内存里。在 Windows 平台,主流的做法有三条路线,我分别说下使用场景和边界。

第一是 GDI。它做的事情本质上是把屏幕画布当成一块位图来做 BitBlt 拷贝,好处是路径短、兼容性好,哪怕老系统也能跑。但它的致命弱点是整块拷贝走 CPU,分辨率一高,CPU 占用就非常难看。1080p 下勉强能撑住 30 帧,到了 4K 基本就要放弃实时编码。所以除非必须要兼容很老的系统,否则我不建议把它作为主力方案。

第二是 DXGI Desktop Duplication。这是从 Windows 8 开始官方提供的桌面复制接口,核心逻辑是让 GPU 直接复制桌面纹理,再通过AcquireNextFrame把新帧取出来。它的效率比 GDI 高一个数量级,而且能拿到桌面光标的位置和形状数据,还能通过DXGI_ERROR_ACCESS_LOST感知分辨率变化或显卡重置。代价是它要求开发者对 Direct3D 11 的设备、纹理、 staging buffer 有基本概念,上手门槛比 GDI 高一些。

第三是 Windows.Graphics.Capture,也就是 WGC。它是 Windows 10 1903 之后推的采集方案,最大的优势是不只支持全屏,还能精确录一个窗口,甚至录被遮挡的窗口内容。配合 Direct3D 互操作也能拿到纹理帧。缺点是 API 套着 Windows Runtime 风格,放到纯 C++ 工程里要做的转换比较多,而且老系统不支持。

我做录屏项目时会按这个规则来排优先级:如果你的目标是教学录制、会议记录这类以全屏为主的场景,DXGI 是当前性价比最高的选择,性能和坑的透明程度都最好;如果产品强调“录某个窗口”,那就无可避免要上 WGC;只有明确要兼容 Windows 7,才回退到 GDI。

下面用一张表把这三种方案的关键差异收拢一下,方便对照:

方案采集开销能否录窗口分辨率变化处理最低系统适合场景
GDICPU 高,内存拷贝仅全屏需重新取 DCWindows XP+老机器兼容
DXGI Desktop DuplicationGPU 复制,开销低仅全屏(可裁剪)返回 ACCESS_LOST 后重建Windows 8+全屏录制的稳定首选
WGCGPU 复制,开销低支持窗口/区域系统自动处理Windows 10 1903+窗口捕获、区域录制

选完之后,DXGI 的最小调用链是固定的:枚举输出设备、创建 D3D11 设备与 IDXGIOutputDuplication、进入循环反复取帧。后面第 4 章会把主循环写出来,这里先记住一个要点:DXGI 取到的纹理默认在 GPU 上,编码器通常读不了,必须经过 staging texture 拷到 CPU 可读内存,这一步不做,后面八成会踩内存或黑帧的坑。

2.2 编码器选型与关键参数:录屏用哪种组合更稳

画面取出来之后就要进编码器。录屏用得最多的是 H.264,兼容性最好,体积可控。实现上分软件编码和硬件编码两条路。

软件编码特指用 x264 这类 CPU 编码器。质量上限高、参数透明、不受显卡驱动影响,录屏首推。缺点就是吃 CPU,在低配机器上如果同时开着 IDE、浏览器和录制器,帧率会变得不稳定。应对办法是调 preset,通常我不会用 slower 这类追求极限压缩的档位,而是定在 veryfast 到 medium 之间,画质损失肉眼基本看不出,CPU 压力却小很多。

硬件编码指调用显卡内置的编码单元,像是 NVENC、AMF、QSV 这些,CPU 占用极低,适合游戏录制或直播场景。但它有几个隐藏问题:一是同一块 GPU 既要负责桌面复制又要负责编码,显存带宽和计算单元会争抢,部分场景下画面反而变卡;二是硬件编码器的低延迟参数和画质参数没有统一标准,换一台显卡可能就得重新调一套参数。所以我的建议是:默认先跑通 x264,等整个录屏链路稳定之后,再考虑加硬编选项。

编码参数里,录屏和视频点播有个关键差异:录屏更强调低延迟,所以要把 B 帧关掉,也就是编码器里的bframes=0。B 帧虽然能提升压缩率,但会引入重排序延迟,一旦音画同步逻辑没做好,就会看到口型对不上的现象重演。

容器格式上,我建议优先用 MP4。但要注意,MP4 的索引写在文件末尾,如果录制过程中程序崩溃,这个文件基本就废了。所以更稳的做法是:先封装成 MKV 或 FLV,录制结束后再用 FFmpeg 转封装成 MP4。这个习惯能省掉很多“录到一半死机,素材全部丢失”的心痛时刻。

音频编码则简单很多,AAC 配合 48kHz 双声道是录屏默认配置,码率给到 128kbps 到 192kbps 就够了,不需要更高。这节先立住了采集和编码的主轴,下面要处理的,就是这两条线合成一个文件时最容易暴露的问题:音画同步。

3. 音画同步:录屏翻车重灾区的修复路线

只要录屏时长超过五分钟,音画同步就是第一个冒出来的硬骨头。它的问题永远在细节里,我们逐个解决。

3.1 音频采集链路:麦克风、系统声音与混音

录屏软件要录的音频通常有两路:系统播放的声音和麦克风的声音。系统声音要用回环采集实现,在 Windows 上走 WASAPI 的 loopback 模式,把声卡输出端的数据反向抓一份出来。麦克风就是常规的采集。

两条路同时开的时候,有一个天然矛盾:系统声音和麦克风来自不同的物理设备,启动时间不同、时钟漂移不同,连采样率都可能不一样。正确的做法是先统一到同一声学参数,我的习惯是把两路都重采样到 48kHz / 16bit / 双声道,然后混音。

混音里面有个最常见的错误:直接把两路 PCM 样本逐点相加。如果两路数据在时间上没对齐,相加之后的人声会带着回声感或者“发虚”。正确的做法是给麦克风一路做一个 30ms 到 50ms 的 FIFO 缓冲,让系统声音先到,麦克风延后到同一个批次,再按样本逐点相加并做一次防止削波的缩放。这个延迟量在实际使用中听感几乎无感,却能明显改善混音质量。

音频能顺利进混音器之后,下一件事就是给音频打时间戳。我看到不少录屏项目是把时间戳放在了“混音完成之后”才生成,这是错的。音频的参考时间应该在数据从设备回调出来的那一刻就拿到,等混音完成之后再生成时间戳,实际上已经晚了几十毫秒,这几十毫秒在长视频里会被反复叠加。

3.2 时间戳模型:用一个时钟贯穿音画

音画同步问题从根上讲只有一个原因:视频帧的时间基准和音频包的时间基准不是同一条时间轴。

比如视频用系统时间GetSystemTimePreciseAsFileTime打点,而音频用声卡设备时钟打点。系统时间可能因为用户调整时钟而跳动,声卡时钟又天然存在漂移,两条轴不可能完全对齐,录得越久偏差越大。

解决的办法是建立一条单调递增的“主时间轴”。在 Windows 上我倾向于用 QueryPerformanceCounter 作为原始来源,因为它单调、精度高、不受系统调时影响。然后让视频帧和音频回调都换算到这条轴上。

视频侧的做法是:每次AcquireNextFrame拿到一帧,立刻记录当前 QPC 时间,并换算成微秒,作为这一帧的 PTS。音频侧的做法是:WASAPI 回调里会告诉我们这一批数据对应的设备时钟位置,我先把设备时钟转换成 QPC 时间,再统一转成微秒。两边的时间基准只要是同一个,音画同步就完成了 80%。

剩下 20% 是播放端和缓冲策略的问题。一些播放器在本地播放 MP4 时会因为 seek 操作重新排布缓冲,造成短暂的音画错位,这不一定是录制文件的错误,最好拉到 VLC 这类播放器里做二次验证。

3.3 长时录制中的漂移消除

即使时间基准统一了,长时间录制仍然可能漂移。声卡硬件时钟和系统 QPC 计时器之间存在微小的频率差异,时间一长就会累积出可见偏差。

处理漂移有几个业界通用手法。第一,不要把音频 PTS 完全交给声卡时钟,而是以系统时钟为准,让音频在送入编码器前做一次重采样对齐。第二,每隔一段时间(比如 600 个视频帧)重新校准一次 PTS 基准,把偏差归零。第三,如果发现音频持续偏离,可以通过调整重采样器的采样率来“追上”视频,比如把 48000Hz 调整为 48004Hz,微调之后偏移会被逐步吸收。

这些逻辑听上去复杂,但放到代码里其实只有几十行。我在第 4 章给主循环时间戳和音频回调时会直接体现这套模型。

4. 把最终完善落进代码:主循环、音频回调与参数表

有了前面的基础,这一章直接给骨架代码。代码刻意做了裁剪,去掉与业务有关的弹窗、设置界面等,只保留录屏最核心的调用顺序。

4.1 主循环的完整骨架

下面的代码基于 DXGI 采集加 FFmpeg 编码,全部跑在 C++17 下。核心是采集循环的状态机,总共分三层:取帧、复制、送编码。

// CaptureLoop.cpp: 录屏主循环骨架 // 前提: d3d_device_ 已创建, duplication_ 已初始化 void CaptureSession::RunLoop() { while (running_) { DXGI_OUTDUPL_FRAME_INFO frame_info = {}; IDXGIResource* desktop_resource = nullptr; HRESULT hr = duplication_->AcquireNextFrame( 500, // 超时 500ms, 无新画面时等待 &frame_info, &desktop_resource); if (hr == DXGI_ERROR_WAIT_TIMEOUT) { // 桌面没有变化, 直接跳过, 不产生冗余帧 continue; } if (hr == DXGI_ERROR_ACCESS_LOST) { // 分辨率变化/显卡重置/锁屏解锁都会触发 RecreateDuplication(); continue; } // 1. 把 GPU 上的纹理拷贝到 CPU 可读的 staging texture CopyToStagingTexture(desktop_resource, frame_info); // 2. 现在 staging_texture_ 里的数据可以供 CPU 读取 // 取帧时刻的 QPC 时间就是这一帧的 PTS uint64_t pts_us = QpcToUs(QpcNow()); // 3. 交给编码器, 内部会调用 avcodec_send_frame encoder_->WriteVideoFrame(staging_texture_, pts_us, frame_info.AccumulatedFrames); // 4. 释放 DXGI 帧, 否则缓冲会堆积 duplication_->ReleaseFrame(); desktop_resource->Release(); } }

这段代码里有三个参数值得展开说。第一个是AcquireNextFrame的超时时间设为 500ms,原因是桌面长时间静止时,DXGI 不会产生新帧,如果不设置超时,循环会被无限阻塞;500ms 既能保证响应及时,又不会频繁空转。第二个是AccumulatedFrames,这是 DXGI 自己统计的累计帧数,可以用它来检测录屏中是否存在跳帧。第三个是ReleaseFrame,这一步漏掉的话,D3D 资源会被占满,录制 20 分钟左右就会开始卡顿。

时间戳转换函数QpcToUs内部做的事情是:把 QueryPerformanceCounter 的计数值除以计数器频率,再乘以 1000000 得到微秒。这个换算只要做一次,整套视频的 PTS 就统一了。

4.2 音频回调与混音骨架

音频侧的核心工作是在 WASAPI 回调里完成时间戳标记和混音入队。下面的代码简化自一套可运行的混音链路,省略了设备初始化细节,保留最关键的时间戳处理逻辑。

// AudioSession.cpp: WASAPI 回调里处理声音数据 void OnAudioSamples(bool is_system_loopback, BYTE* data, UINT32 frames_available, uint64_t device_clock) { // 1. 把设备时钟统一到系统 QPC 时间轴 uint64_t pts_us = DeviceClockToQpcUs(device_clock); // 2. 重采样到统一格式(48kHz/16bit/stereo) resampler_.Resample(data, frames_available, device_rate_); // 3. 按来源分别进入混音器的两个输入队列 if (is_system_loopback) { mixer_.Push(kSystemInput, resampler_.GetOutput()); } else { mixer_.Push(kMicInput, resampler_.GetOutput()); } // 4. 攒够 10ms 数据后取出一块 PCM 送去编码 // 这个缓冲就是混音对齐的核心, 麦克风会比系统声音晚 30ms 到达 if (mixer_.HasEnoughForPacket(480)) { // 480 个样本 = 10ms @ 48kHz auto pcm = mixer_.TakeMixedPacket(); encoder_->WriteAudioFrame(pcm, pts_us); } }

为什么要把麦克风延迟 30ms?因为麦克风和系统声音走到同一块逻辑时,系统声音是直接从声卡回环取出来的,延时就 1-2ms;麦克风经过声卡采集、驱动缓冲,实际拿到时已经晚了 20ms 以上。如果不做对齐,两路声音直接相加,人声会像站在山洞里说话。这里的 30ms 不是固定的,我会写成一个可调参数,让用户在实际反馈中微调。

音频 PTS 用设备时钟转 QPC 时间,视频 PTS 用取帧时刻的 QPC 时间,二者出自同一个频率源,频率漂移问题就控制在了一个很小的范围内。如果还要追求极致,可以在视频 PTS 与音频 PTS 之间做一次渐进修正,但大多数场景上一步已经够用。

4.3 必备参数表:录屏软件最终完善的关键收口

录屏软件真正值得反复打磨的其实就下面这些参数,我把推荐值和踩过的坑一并列出来,方便对照检查。

参数推荐值说明与坑
视频帧率30fps 教学 / 60fps 游戏帧率过高会推高码率,低配机器建议 30
视频码率8000kbps CBR(1080p30)码率给太高会撑爆写入速度,给太低动态画面会糊
B 帧0录屏必须关 B 帧,否则同步延迟增加
GOP 间隔帧率 × 22 秒一个关键帧,便于剪辑和断点续录
x264 presetveryfast平衡质量与 CPU 占用,追求画质再降到 medium
音频采样率48kHz统一格式,避免重采样导致的音质损耗
音频码率160kbps对白场景足够,不用再高
混音缓冲30ms麦克风延迟补偿,具体值按实际设备调整
容器格式录制用 MKV / 交付转 MP4防崩溃丢索引,转封装成本几乎为零

这张表看起来简单,每一个都是实际踩过坑才定下来的。比如说 8000kbps 这个值,它是“画质基本无损、体积不太离谱、写入速度普通硬盘扛得住”三者之间的平衡点,想抄作业就直接用,后续再按自己的场景上下浮动。

5. 录屏避坑指南:五个高频问题与排查路径

这一章是我自己反复踩过的坑,全部按“现象 → 原因 → 解决”的格式写,方便你遇到问题时对号入座。

5.1 黑屏或黑帧:采集能跑,录出来什么都看不见

现象:录制过程中预览正常,播放录像却只有黑屏,或者前几秒正常然后变黑。

原因:最常见的是 staging texture 的格式与 DXGI 输出的纹理格式不匹配。DXGI 桌面纹理通常是 DXGI_FORMAT_B8G8R8A8_UNORM,如果后面转成 RGB24 时没有正确处理,就会得到全黑数据。另一大类原因是安全桌面,比如录制时弹出了 UAC 权限确认框或进入锁屏,DXGI 会直接停止输出,恢复后如果没有重建采集器,后面录进去的全是黑帧。

解决:先确认 CopyToStagingTexture 里没有擅自改像素格式,保存原始格式到编码器再做转换;然后监听 ACCESS_LOST,触发时重建整套采集资源。这两个修完,黑屏基本能解决。

5.2 音画不同步:录得越久,偏移越大

现象:前 5 分钟音画正常,录到 20 分钟时人物口型已经慢了一秒。

原因:这个坑我在第 3 章讲过根因。90% 的情况是音频用了声卡时钟,视频用了系统时间,两者频率不一致,而且偏差是持续累积的。还有 10% 的情况是播放器的问题,一些播放器在渲染长视频时会重新调整音频缓冲。

解决:统一时间基准到 QPC,并给音频加漂移修正逻辑。修正完之后再验证一次:用播放器的逐帧模式检查一段有明显瞬态的画面,比如拍手或敲键盘的声音画面,误差应在 50ms 以内。

5.3 内存与句柄泄漏:录到一半软件卡死

现象:软件刚启动占用 80MB 内存,录了半小时后涨到 1GB 以上;或者 GDI 对象数持续上升。

原因:DXGI 帧释放遗漏是最常见的。AcquireNextFrame 之后拿到的 COM 资源必须 Release,staging texture 每次使用时也要确保不重新创建而是复用,否则驱动会在后台积累大量显存资源。另一个隐蔽点是 FFmpeg 编完一帧之后,AVPacket 没有及时 unref,也造成内存只增不减。

解决:给 DXGI 取帧的代码段加上 RAII 封装,确保所有路径都能释放;编码器每发送一个 AVPacket 后立刻 unref;最后在录制循环里每帧记录内存占用,如果发现 10 分钟内持续上涨但样本并未增多,就要立即检查资源释放路径。

5.4 分辨率变化与睡眠唤醒导致的采集中断

现象:用户把显示器从 1080p 切到 4K,或者电脑睡眠唤醒之后,录屏画面冻结甚至直接崩溃。

原因:DXGI 的 Desktop Duplication 在显示模式变化后,已经建立的 duplication 对象会失效,必须重建。睡眠唤醒也是同理,而且唤醒瞬间显卡上下文可能已经丢失。

解决:把DXGI_ERROR_ACCESS_LOST当成正常的“需要重建”的信号,不要当成致命错误。重建时要依次释放旧 duplication、重新枚举输出、重新创建 duplication。这里有一个小坑:重建完成后,桌面可能因为 DWM 重绘而闪过一帧黑画面,可以把重建完成后的第一帧丢弃,避免录到花屏。

5.5 光标丢失或闪烁

现象:系统鼠标指针在录像里时有时无,或者出现残影。

原因:DXGI 的 Desktop Duplication 可以单独输出鼠标指针的形态和位置,但它不会把指针合成到画面纹理里。如果代码里没做指针绘制,指针就永远不在输出中;如果绘制了但位置没跟新帧同步,指针就会闪。

解决:用frame_info.PointerPosition和frame_info.PointerShapeBuffer拿到指针的当前位置和形状,然后在每一帧里把指针合成到图像对应坐标。注意要在视频帧的 PTS 同一时刻读取指针位置,不要提前或延后。指针合成最好用预乘透明度处理,否则圆角边缘会出现黑边。

6. 最后打磨:自动分段录制与三项验收指标

功能都完善之后,最后值得加的一项能力是自动分段录制,以及用一组可量化的指标确认这套录屏方案可以正式投入使用。

6.1 自动分段录制:避免单文件过大与损坏

长时间录制时,单文件越来越大,一旦程序崩溃损失就大,而且部分编码器在超过 4GB 时封装格式会出现异常。因此成熟录屏软件都会做分段。

实现思路不复杂:以时间或文件大小作为阈值,达到阈值时,在当前 GOP 边界完成一个分段的写入,然后开启新的输出文件。关键是分段不能破坏音画同步,所以新文件的 PTS 不要清零,而是在新文件的时间戳基础上加上前一个文件的总时长偏移,这样后续拼接时不会乱序。

我个人更推荐按大小分段而不是按时间分段,因为 4GB 是 MP4 格式的安全线,超过这个阈值,即使播放器能放,后期剪辑软件也可能拒绝导入。

6.2 交付前建议跑这三项验收指标

第一项是长时间稳定性测试。连续录制 20 分钟动态内容,观察内存增量不超过 50MB、帧率波动不超过设定值的 10%。如果内存曲线一直向上走,说明资源释放还有问题。第二项是音画同步精度测试。录一段有清晰瞬态声音的操作过程,比如边敲键盘边说话,然后在播放器里逐帧比对,音画偏差不超过 50ms 算合格。第三项是资源占用测试。在录屏的同时跑一个常规办公场景,CPU 占用不能长期超过 30%,否则用户开着其他软件就会明显卡顿。

我自己的习惯是:录屏软件做完这轮完善之后,先用它录制三天的线上课程,每一段都超过 40 分钟。只有真的把自己日常使用的场景都覆盖到,那些只在特定显卡、特定分辨率下才冒出来的问题才会暴露出来。录屏软件没有“做完”的那一天,但达到这三项指标,至少可以放心拿出去给别人用了。希望这一讲能帮你少走几段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询