☰
屏幕录像程序源代码实战:抓屏、编码与封装全解析
2026/10/17 8:20:44 网站建设 项目流程

简介:这份屏幕录像程序源代码面向具备一定C++与Windows开发基础的开发者,用于学习或二次开发屏幕录制工具。资源围绕主窗体设计、录像设置、屏幕捕获与编码、截图、文件播放与重命名、时长查看等模块展开,并涉及错误处理、性能优化及版权隐私提示,适合教程制作、游戏录制、软件演示等场景。压缩包共58个文件,约5.61MB,以cpp与h源码文件为核心,配合ico图标、sbr、obj、pdb等编译中间文件,以及dsw、dsp工程文件和doc使用说明,便于直接导入开发环境调试。目前已有596人学习下载。通过研读源码,读者可掌握GDI+或DirectX屏幕捕获、H.264编码、文件管理与播放器组件交互等关键实现,理解各模块协作方式,并借鉴性能优化与异常处理思路,快速搭建可运行的屏幕录像程序。

1. 屏幕录像程序源代码:从录屏工具到可复现的工程骨架

很多人第一次搜「屏幕录像程序源代码」,脑子里想的其实是两件事:一是想找一个能直接跑的录屏工具,二是想弄明白录屏这件事到底是怎么实现的,能不能自己改。我当年也是这么想的,结果下载了一堆代码包,编译报错、帧率掉到个位数、音频视频对不上,折腾了整整一周才跑通第一个能用的版本。屏幕录像程序源代码的核心,说白了就是三件事:抓屏、编码、封装。抓屏决定你能不能拿到画面,编码决定文件大不大、卡不卡,封装决定音视频能不能同步、能不能被播放器认。这套东西适合谁?适合想自己做一个轻量录屏工具、想给现有软件加录屏功能、或者想搞清楚桌面采集底层原理的开发者。它不复杂,但坑很密,下面我按实际落地的顺序,把选型、实现、参数和排错一条条拆开讲。

2. 抓屏方案怎么选:GDI、DXGI 与跨平台采集的取舍

抓屏是整个屏幕录像程序源代码里最底层的一环,选错了后面全白搭。我见过太多人一上来就抄一段 GDI 的 BitBlt 代码,结果在 1080p 下只能跑到 10 帧,还以为是编码慢,其实是抓屏方式本身就不对。这一章先把抓屏的几条主流路线讲清楚,再落到具体代码。

2.1 三种抓屏方式的真实差距

Windows 上常见的抓屏方式有三种:GDI、DXGI Desktop Duplication、Windows Graphics Capture。GDI 最老,兼容性最好,但它是把桌面当成一个普通 DC 来拷贝,每次都要走一遍内存拷贝,分辨率一高就撑不住。DXGI Desktop Duplication 是 DirectX 提供的桌面复制接口,直接拿 GPU 里的帧,效率高很多,但要求系统是 Win8 以上,且在某些多显卡、远程桌面场景下会失败。Windows Graphics Capture 是 Win10 1803 之后的新接口,能抓单个窗口,也能抓整个屏幕,性能介于两者之间,但 API 相对新,老项目里用得少。

跨平台的话,Linux 一般走 X11 的 XShm 或者 Wayland 的 PipeWire,macOS 走 CoreGraphics 的 CGDisplayStream。如果你要做跨平台,建议把抓屏抽象成一个接口,每个平台实现一份,别想着用一套代码通吃。

抓屏方式平台典型帧率(1080p)主要限制
GDI BitBltWindows8~15 fps内存拷贝重,高分辨率掉帧
DXGI Desktop DuplicationWindows 8+60 fps 以上远程桌面、多显卡可能失败
Windows Graphics CaptureWindows 10 1803+30~60 fpsAPI 较新,窗口捕获需权限
X11 XShmLinux30~60 fpsWayland 下不可用
CoreGraphicsmacOS30~60 fps需要屏幕录制权限

选型建议很直接:Windows 上优先 DXGI,失败时降级到 GDI;跨平台就按平台各写一份。别在 GDI 上死磕性能,那是方向错了。

2.2 用 DXGI 抓一帧的最小代码

下面这段是 DXGI Desktop Duplication 抓一帧的核心逻辑,我把它简化到能直接看懂的程度。实际项目里你会把它放进一个循环,但先跑通一帧很重要。

// 初始化 D3D11 设备和 DXGI 输出复制 IDXGIOutputDuplication* duplication = nullptr; IDXGIResource* desktopResource = nullptr; DXGI_OUTDUPL_FRAME_INFO frameInfo; // 抓一帧,超时 500ms HRESULT hr = duplication->AcquireNextFrame(500, &frameInfo, &desktopResource); if (hr == DXGI_ERROR_WAIT_TIMEOUT) { // 这 500ms 内画面没变化,直接返回,别当成错误 return; } if (FAILED(hr)) { // 常见失败:DXGI_ERROR_ACCESS_LOST,需要重新初始化 duplication ReinitDuplication(); return; } // 拿到纹理,映射到 CPU 可读内存 ID3D11Texture2D* texture = nullptr; desktopResource->QueryInterface(__uuidof(ID3D11Texture2D), (void**)&texture); D3D11_MAPPED_SUBRESOURCE mapped; context->Map(texture, 0, D3D11_MAP_READ, 0, &mapped); // mapped.pData 就是这一帧的像素数据,pitch 是行跨度 // 注意:pitch 不一定等于 width * 4,拷贝时要按行处理 CopyFrameToBuffer(mapped.pData, mapped.RowPitch, width, height); context->Unmap(texture, 0); duplication->ReleaseFrame();

这段代码里几个关键点:AcquireNextFrame的超时不是错误,画面没变化时它会返回DXGI_ERROR_WAIT_TIMEOUT,这时候直接跳过就行,别当成异常处理。DXGI_ERROR_ACCESS_LOST才是真错误,通常发生在分辨率切换、全屏切换或者远程桌面连接时,遇到就重新初始化 duplication。RowPitch是行跨度,可能大于width * 4,拷贝时必须按行拷贝,否则画面会斜。

2.3 抓屏循环的节奏控制

抓屏不是越快越好。你抓得再快,编码器吃不下也是白搭。我一般会把抓屏循环和编码循环分开,中间用一个有界队列缓冲。抓屏线程按固定间隔抓,比如目标 30 帧就每 33ms 抓一次,抓到就丢进队列;编码线程从队列取,取不到就等。队列满了就丢最旧的帧,保证延迟不累积。

// 抓屏线程:固定节奏抓帧,丢进有界队列 while (running) { auto frame = CaptureFrame(); if (frame) { // 队列容量 3,满了丢最旧的,避免延迟堆积 frameQueue.push(std::move(frame)); } std::this_thread::sleep_for(std::chrono::milliseconds(33)); }

队列容量别设太大,3 到 5 就够了。设大了延迟会累积,观众看到的是几秒前的画面,体验很差。这个细节很多开源录屏代码里没处理好,录出来的文件看着没问题,但直播场景下延迟爆炸。

3. 编码与封装:把抓到的帧变成能播的文件

抓屏拿到的是原始像素,一帧 1080p 的 BGRA 数据就是 1920×1080×4 约 8MB,30 帧一秒就是 240MB,不编码根本没法存。编码和封装是屏幕录像程序源代码里第二个大头,选错了要么文件巨大,要么播放器不认。

3.1 软编还是硬编:先看你的场景

软编就是 x264、x265 这类纯 CPU 编码,硬编就是 NVENC、QSV、AMF 这些走 GPU 的。软编画质好、参数细,但 CPU 占用高,1080p 30 帧大概要吃掉两三个核心。硬编 CPU 占用低,但画质略差,参数也没那么灵活。

我的建议是:如果你做的是本地录屏工具,优先硬编,把 CPU 留给别的活;如果你做的是转码或者对画质要求极高,用软编。判断硬编能不能用,先跑一下ffmpeg -encoders | grep nvenc看有没有对应编码器。

# 查看当前 ffmpeg 支持哪些硬件编码器 ffmpeg -hide_banner -encoders | grep -E "nvenc|qsv|amf" # 用 NVENC 编码一帧测试,确认硬编可用 ffmpeg -f rawvideo -pix_fmt bgra -s 1920x1080 -r 30 -i - \ -c:v h264_nvenc -preset p4 -b:v 4M -f null -

-preset p4是 NVENC 的预设,p1 最快画质最差,p7 最慢画质最好,一般用 p4 或 p5。-b:v 4M是目标码率,1080p 30 帧录屏 4M 到 8M 都算合理,静态画面多可以降到 2M。

3.2 用 FFmpeg 做编码和封装的完整命令

实际项目里我很少直接调 FFmpeg 命令行,但调试阶段用命令行验证参数是最快的。下面这条命令把原始 BGRA 帧编码成 H.264 并封装成 MP4。

ffmpeg -f rawvideo -pix_fmt bgra -s 1920x1080 -r 30 -i - \ -c:v libx264 -preset veryfast -crf 23 -g 60 \ -pix_fmt yuv420p -movflags +faststart \ output.mp4

-crf 23是软编的质量参数,18 到 28 之间,越小画质越好文件越大。-g 60是关键帧间隔,30 帧下就是每 2 秒一个关键帧,方便拖动。-pix_fmt yuv420p是兼容性最好的像素格式,不转的话很多播放器打不开。-movflags +faststart把索引放到文件头,适合边下边播。

如果你用硬编,把-c:v libx264 -preset veryfast -crf 23换成-c:v h264_nvenc -preset p4 -b:v 6M就行。注意硬编一般用码率控制,不用 crf。

3.3 音视频同步:时间戳怎么打

录屏最烦的就是音视频不同步,画面比声音快或者慢半拍。根因通常是时间戳没打好。抓屏线程和音频采集线程各自有各自的时钟,你得用一个统一的时间基准,比如都用系统启动后的毫秒数。

// 抓屏时记录帧的 PTS,单位毫秒 auto now = std::chrono::steady_clock::now(); int64_t pts_ms = std::chrono::duration_cast<std::chrono::milliseconds>( now - startTime).count(); // 音频采集同样用 steady_clock 打 PTS // 封装时把视频 PTS 和音频 PTS 都交给封装器

关键点是视频和音频必须用同一个时钟源,别一个用system_clock一个用steady_clock。另外音频采样率要和编码器对齐,常见是 44100 或 48000,别中途变。如果发现同步漂移,先查时钟源,再查采样率,最后查封装时的 PTS 单位是不是一致。

4. 避坑与排查:录屏代码里最容易翻车的五个地方

这一章是我自己踩过的坑,也是看别人代码时最常发现的问题。每条按现象、原因、解决来写,你对着查就行。

4.1 录出来的视频画面是黑的

现象:文件能播,但画面全黑,只有鼠标或者某个窗口有内容。

原因:DXGI 抓的是桌面合成后的画面,但某些全屏独占程序、受保护内容(比如某些播放器)会绕过桌面合成,抓不到。另外如果 D3D 设备创建时没选对适配器,也可能抓到空纹理。

解决:先确认是不是受保护内容,是的话换 Windows Graphics Capture 试试。如果是适配器问题,枚举所有适配器,选那个连着主显示器的。代码里加个判断,如果连续多帧都是全黑,降级到 GDI 抓屏。

4.2 帧率上不去,CPU 却不高

现象:目标 30 帧,实际只有 12 帧,但 CPU 占用只有 20%。

原因:多半是抓屏循环里做了同步等待,比如每帧都sleep固定时间,但抓屏本身耗时波动,导致实际间隔远大于预期。也可能是队列满了在丢帧,但你没打日志。

解决:把抓屏和编码彻底解耦,抓屏线程只管抓,抓到就丢队列,不等待。队列满了丢帧时打一条日志,看看丢帧率。如果丢帧率高,说明编码跟不上,降分辨率或者换硬编。

4.3 音视频不同步,越录越偏

现象:开头同步,录到后面声音比画面慢了几百毫秒。

原因:音频采集的采样率和实际时钟有偏差,或者视频 PTS 用的是帧计数而不是真实时间。长时间录制时,微小的偏差会累积。

解决:音频采集用真实时钟打 PTS,别用采样数除以采样率算。视频 PTS 也用真实时钟。封装时如果发现漂移,可以在音频里做重采样补偿,但根治还是时钟源要统一。

4.4 硬编初始化失败,报错看不懂

现象:调用 NVENC 初始化返回错误,但错误码查不到具体原因。

原因:常见的是驱动版本太老、显卡不支持当前编码格式、或者同时打开的编码会话数超了。消费级显卡 NVENC 并发会话数有限制,一般是 2 到 3 个。

解决:先更新驱动,再确认显卡支持的编码格式。如果并发超了,复用同一个编码会话,别每个录制任务都新建。代码里把错误码打出来,对照厂商文档查。

4.5 录出来的文件播放器不认

现象:文件大小正常,但某些播放器打不开,或者只有声音没画面。

原因:像素格式不是 yuv420p,或者封装格式和编码格式不匹配,比如把 H.265 封进某些老容器。也可能是 moov 原子在文件末尾,播放器要下完才能播。

解决:编码时强制-pix_fmt yuv420p,封装用 MP4 就配 H.264,用 MKV 兼容性更宽。加-movflags +faststart把索引提前。如果还不行,用ffprobe看文件信息,对比正常文件差在哪。

5. 进阶技巧:把录屏延迟压到 100ms 以内的几个手段

前面讲的都是能跑通的程度,如果你要做直播或者远程协作,延迟就是命门。我做过一个远程演示的场景,要求端到端延迟 100ms 以内,最后压到了 80ms 左右,靠的是几个具体手段。

第一个手段是抓屏用 DXGI 的AcquireNextFrame带超时,但超时设短,比如 16ms,这样画面有变化立刻返回,没变化也不空等。第二个手段是编码用硬编的低延迟预设,NVENC 有-tune ll低延迟模式,QSV 也有对应参数,开了之后编码器不做 B 帧、不做前向参考,延迟直接降一截。

# NVENC 低延迟配置 ffmpeg -f rawvideo -pix_fmt bgra -s 1920x1080 -r 60 -i - \ -c:v h264_nvenc -preset p1 -tune ll -b:v 8M -g 30 \ -f mpegts - | your_stream_sender

-tune ll是关键,它让编码器走低延迟路径。-g 30在 60 帧下是每 0.5 秒一个关键帧,关键帧间隔短,丢包恢复快,但码率会高一点。-f mpegts是传输流格式,适合实时传输,别用 MP4,MP4 的索引结构不适合流式。

第三个手段是传输层用 UDP 而不是 TCP,TCP 的重传机制在丢包时会引入额外延迟。但 UDP 要自己处理丢包和乱序,一般用 RTP 或者基于 UDP 的可靠协议。这块超出录屏本身了,但如果你延迟压不下去,多半是传输层拖后腿。

第四个手段是渲染端做抖动缓冲,但缓冲别设大,2 到 3 帧就够,设大了延迟又上去了。缓冲大小要根据网络抖动动态调,网络稳就调小,抖就调大。

验证延迟的方法很简单:手机秒表放在摄像头前,录屏同时拍屏幕和秒表,看两个时间差。我一般测三次取平均,80ms 到 120ms 都算正常,低于 80ms 就很优秀了。

最后说个我自己的习惯:每次改完抓屏或者编码参数,先录 10 秒静态画面,再录 10 秒快速滚动画面,对比文件大小和帧率。静态画面看码率控制,滚动画面看有没有撕裂和丢帧。这个习惯帮我省了很多次返工。希望帮到你。

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

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

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

立即咨询