简介面向音视频编程入门与进阶学习者这是一套完整的录屏软件开发工程基于 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 帧关掉也就是编码器里的bframes0。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 编码全部跑在 C17 下。核心是采集循环的状态机总共分三层取帧、复制、送编码。// 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 CBR1080p30码率给太高会撑爆写入速度给太低动态画面会糊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 资源必须 Releasestaging 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 分钟。只有真的把自己日常使用的场景都覆盖到那些只在特定显卡、特定分辨率下才冒出来的问题才会暴露出来。录屏软件没有“做完”的那一天但达到这三项指标至少可以放心拿出去给别人用了。希望这一讲能帮你少走几段弯路。本文还有配套的精品资源点击获取