简介:这份屏幕录像程序源代码面向具备一定C++与Windows开发基础的程序员、软件专业学生及需要自研录屏工具的开发者,帮助其理解屏幕捕获、视频编码与文件管理的完整实现思路,可用于教程制作、游戏录制或软件演示等场景。资源包共58个文件,约5.61MB,以cpp与h源文件为核心,配合ico图标、sbr浏览信息、obj与pdb调试文件、dsw与dsp工程文件及rc资源脚本,另附程序使用说明文档,构成一套可直接编译研究的工程结构。目前已有596人学习下载。代码围绕主窗体交互、录像参数设置、屏幕帧捕获与H.264编码、截图保存、录制文件播放与重命名删除等模块展开,并涉及GDI+或DirectX捕获、错误处理、性能优化及隐私提示等设计考量,读者可据此掌握录屏软件从界面到编码算法的关键实现,并在此基础上进行二次开发与功能扩展。
1. 屏幕录像程序源代码:从录屏工具到可复用工程骨架
很多人第一次搜「屏幕录像程序源代码」,脑子里想的是找一个能跑的录屏小工具,改改标题就交付。但真正做过桌面端录屏的人都知道,能录和录得稳之间隔着一整条工程化鸿沟。屏幕录像程序源代码的核心价值不在于「能抓到画面」,而在于它把采集、编码、封装、音画同步、性能控制这几件事串成了一条可维护的流水线。你拿到一份录屏源码,真正要评估的是它的采集频率怎么控制、编码器怎么选、音视频时间戳怎么对齐、掉帧了怎么补。这套东西适合两类人:一类是想在自家产品里嵌入录屏能力的桌面应用开发者,另一类是需要在特定场景(比如教学演示、自动化测试回放、远程协作标注)里定制录制逻辑的工程师。如果你只是想要一个现成 exe,那没必要看源码;但如果你要改采集区域、换编码参数、加自定义水印或者对接自己的存储管线,那读懂屏幕录像程序源代码就是绕不过去的一步。
2. 录屏源码的四个核心模块与选型逻辑
2.1 采集层:GDI、DXGI 与跨平台方案的取舍
屏幕录像程序源代码里最底层也最要命的就是采集层。Windows 上常见三条路:GDI 的 BitBlt、DXGI 的 Desktop Duplication API、以及基于图形钩子的方案。GDI 最老也最简单,兼容性极好,但它是 CPU 拷贝,1080p 60fps 下 CPU 占用能飙到 20% 以上,而且遇到独占全屏或者硬件加速窗口容易采到黑屏。DXGI Desktop Duplication 是现在的主流选择,它直接从 GPU 的交换链拿纹理,CPU 占用低,能拿到真实的帧变化,但要求系统是 Win8 以上,且在某些多显卡切换场景下需要处理适配器丢失的问题。
跨平台方案里,Linux 一般走 X11 的 XShm 或者 Wayland 的 PipeWire,macOS 走 ScreenCaptureKit 或老的 CGDisplayStream。如果你拿到的源码只写了 GDI,那它大概率是个教学 Demo,直接上生产会翻车。我一般会先看采集层用的是哪套 API,再决定要不要继续往下读。
// 以 DXGI Desktop Duplication 为例,初始化采集设备的核心逻辑 IDXGIOutputDuplication* duplication = nullptr; ID3D11Device* device = nullptr; ID3D11DeviceContext* context = nullptr; // 创建 D3D11 设备,注意这里要指定 BGRA 支持 D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, D3D11_SDK_VERSION, &device, nullptr, &context); // 获取 DXGI 设备并枚举输出 IDXGIDevice* dxgiDevice = nullptr; device->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgiDevice); IDXGIAdapter* adapter = nullptr; dxgiDevice->GetAdapter(&adapter); IDXGIOutput* output = nullptr; adapter->EnumOutputs(0, &output); // 0 表示主显示器 // 拿到 Output1 接口才能创建 Duplication IDXGIOutput1* output1 = nullptr; output->QueryInterface(__uuidof(IDXGIOutput1), (void**)&output1); output1->DuplicateOutput(device, &duplication);这段代码的关键参数是D3D_DRIVER_TYPE_HARDWARE,它保证走 GPU 路径。如果你改成D3D_DRIVER_TYPE_WARP,那就是软件渲染,性能会掉一个数量级。EnumOutputs(0)里的 0 是显示器索引,多屏场景下要遍历。DuplicateOutput返回后,你还需要一个循环去AcquireNextFrame,每次拿到帧后必须ReleaseFrame,否则几帧之后就会卡死。这是新手最容易踩的坑:忘了释放帧,程序跑几秒就黑屏。
2.2 编码层:软编、硬编与实时参数调优
采集拿到的是原始 BGRA 帧,一帧 1080p 就是 8MB 左右,60fps 下每秒 480MB,不编码根本没法存。屏幕录像程序源代码里编码层通常有两种选择:软件编码(x264、OpenH264)和硬件编码(NVENC、QSV、AMF)。软编画质好、参数灵活,但 CPU 占用高;硬编省 CPU,但低码率下画质损失明显,而且不同厂商的编码器行为有差异。
选型上,如果目标是本地录制、对文件体积不敏感,x264 的preset=veryfast加crf=18是稳妥起点。如果要实时推流或者长时间录制,硬编更合适,但要把码率给足,1080p60 建议 8-12 Mbps,否则快速运动的画面会出现明显块效应。
# 用 PyAV 封装 x264 编码器的参数配置示例 import av codec = av.CodecContext.create('libx264', 'w') codec.width = 1920 codec.height = 1080 codec.pix_fmt = 'yuv420p' codec.framerate = 60 codec.bit_rate = 8_000_000 # 8 Mbps codec.options = { 'preset': 'veryfast', # 编码速度与压缩率的平衡点 'crf': '18', # 恒定质量模式,18 接近视觉无损 'tune': 'zerolatency', # 降低编码延迟,适合实时录制 'g': '120', # 关键帧间隔,60fps 下每 2 秒一个 I 帧 'profile': 'high', }这里crf和bit_rate同时给的时候,x264 会优先走 CRF 模式,bit_rate只作为 VBV 的上限参考。tune=zerolatency会关闭 B 帧和 lookahead,画质略有下降但延迟低。g=120控制关键帧密度,太小文件变大,太大则拖动进度条时定位不准。如果你发现录出来的视频在快速滚动时糊成一片,先把crf降到 15 或者把preset改成faster试试。
2.3 封装与音画同步:时间戳才是黑匣子
编码后的视频包和音频包要写进同一个容器,常见的是 MP4 或 MKV。MP4 兼容性好但需要 moov box,录制中途崩溃容易丢文件;MKV 容错强,适合长时间录制。屏幕录像程序源代码里最容易出玄学问题的地方就是音画同步。视频帧的时间戳来自采集时的 QPC 计时,音频来自声卡的采样计数,两者基准不同,必须统一到一个时间轴上。
常见做法是:以音频时钟为主时钟,视频帧根据采集时刻映射到音频时间轴。如果音频比视频快,就丢视频帧或者重复帧;如果视频快,就等待。很多开源录屏工具在长时间录制后出现音画漂移,就是因为没有做时钟漂移补偿。
// 简化的音视频时间戳对齐逻辑 struct AVFrameWrapper { int64_t pts; // 统一时间轴上的显示时间戳 int64_t dts; // 解码时间戳 bool is_video; }; // 以音频采样数为基准计算主时钟 int64_t audio_clock = audio_samples * 1000000 / sample_rate; // 微秒 // 视频帧到达时,计算其相对音频时钟的偏移 int64_t video_pts = qpc_to_us(frame_capture_time); int64_t diff = video_pts - audio_clock; if (diff > 40000) { // 视频超前超过 40ms // 等待或重复上一帧 } else if (diff < -40000) { // 视频落后超过 40ms // 丢弃当前帧 } else { frame->pts = video_pts; // 在容忍范围内,直接使用 }40ms 这个阈值不是随便定的,它对应大约 1 帧 25fps 的时长,人眼对超过这个量的音画不同步开始有感知。实际工程里还会用滑动平均来平滑时钟,避免频繁丢帧。如果你录出来的视频人声比画面快半秒,基本就是音频时钟没对齐,检查采集音频时用的采样率是否和编码器声明的一致。
2.4 性能控制:掉帧、内存与线程模型
录屏程序跑起来之后,最怕的就是越录越卡。屏幕录像程序源代码里通常有三类线程:采集线程、编码线程、写文件线程。如果采集和编码在同一个线程,编码一慢就会丢采集帧;如果写文件和编码同线程,磁盘一卡就全堵住。合理的做法是采集线程只负责拿帧丢进队列,编码线程从队列取帧编码,写线程再把编码包落盘。队列要有上限,满了就丢最旧的帧,保证实时性。
内存方面,原始帧的缓冲区要复用,不能每帧都 new 一块。DXGI 的帧需要 CopyResource 到自己的纹理再释放,否则 Duplication 接口会阻塞。这些细节在源码里往往就是几行,但少了这几行,程序跑几分钟就崩。
3. 从源码到可运行:编译、依赖与最小验证
3.1 环境准备与依赖清单
拿到一份屏幕录像程序源代码,第一步不是急着编译,而是把依赖理清楚。Windows 上常见的依赖组合是:Visual Studio 2019/2022、Windows SDK 10、CMake 3.20+。如果源码用了 FFmpeg,还要准备对应的 dev 包和 dll。Linux 上则是 GCC 9+、pkg-config、libx11-dev、libx264-dev、libavformat-dev 这一套。
| 依赖项 | 用途 | 常见版本要求 |
|---|---|---|
| Windows SDK | DXGI、Media Foundation | 10.0.19041 以上 |
| FFmpeg dev | 编码与封装 | 4.4 或 5.x |
| CMake | 构建 | 3.20 以上 |
| Visual Studio | 编译器 | 2019 或 2022 |
| libx264 | 软编 | 最新稳定版 |
我一般会先跑cmake --version和ffmpeg -version确认基础环境,再看源码根目录有没有CMakeLists.txt或.sln。如果只有.sln没有 CMake,那基本是 Windows 专用,跨平台移植成本高。
3.2 编译命令与常见报错处理
以 CMake 工程为例,标准流程是建一个 build 目录,然后配置、编译。
# 在源码根目录下执行 mkdir build && cd build # 配置,指定 FFmpeg 路径和生成器 cmake .. -G "Visual Studio 17 2022" -A x64 \ -DFFMPEG_ROOT="C:/ffmpeg" \ -DCMAKE_BUILD_TYPE=Release # 编译 cmake --build . --config Release --parallel 8-DFFMPEG_ROOT是告诉 CMake 去哪里找 FFmpeg 的头文件和库,不同项目的变量名可能不一样,有的用FFMPEG_DIR,有的用CMAKE_PREFIX_PATH。如果报Could NOT find FFmpeg,先确认路径下有没有include和lib两个目录。--parallel 8是并行编译,数字按 CPU 核心数给,给太大反而会因为内存不足失败。
链接阶段常见的错误是unresolved external symbol,多半是库的顺序不对或者漏了某个 lib。Windows 上 DXGI 需要链接dxgi.lib和d3d11.lib,FFmpeg 需要avcodec.lib、avformat.lib、avutil.lib、swscale.lib。如果源码里用了#pragma comment(lib, ...)就省事,没有的话要自己在 CMake 里补target_link_libraries。
3.3 最小验证:录 10 秒看三件事
编译通过只是第一步,跑起来能不能用要看三件事:画面有没有、声音有没有、音画同不同步。我一般会写一个最小的验证脚本,录 10 秒,然后检查输出文件。
# 运行录屏程序,录制 10 秒 ./screen_recorder --duration 10 --output test.mp4 --fps 30 --audio # 用 ffprobe 检查流信息 ffprobe -v error -show_streams -show_format test.mp4 # 检查视频帧数和音频采样数是否匹配 ffprobe -v error -select_streams v:0 -count_frames \ -show_entries stream=nb_read_frames -of csv=p=0 test.mp4 ffprobe -v error -select_streams a:0 -show_entries stream=duration \ -of csv=p=0 test.mp4--duration 10是录制时长,--fps 30是目标帧率,--audio开启音频。ffprobe 的输出里重点看nb_read_frames是不是接近 300(10 秒 30fps),音频 duration 是不是接近 10 秒。如果视频帧数只有 150,说明采集或编码丢了一半的帧,要回去查队列是不是太小或者编码太慢。如果音频 duration 差了几百毫秒,那就是时钟对齐的问题。
4. 避坑与排查:录屏源码里最容易翻车的五个地方
4.1 录出来全黑或者只有鼠标
现象:视频文件能播放,但画面是纯黑,只有鼠标指针在动。 原因:采集层用了 GDI 的 BitBlt,但目标窗口是硬件加速渲染的,GDI 拿不到实际画面。或者 DXGI 的 Duplication 在独显/核显切换时拿到了错误的适配器。 解决:换用 DXGI Desktop Duplication,并在初始化时遍历所有适配器和输出,找到真正在渲染的那个。如果是多屏,确认录制的是主屏还是副屏。
4.2 录制几分钟后程序卡死
现象:开始正常,几分钟后画面定格,程序无响应。 原因:DXGI 的AcquireNextFrame拿到帧后没有调用ReleaseFrame,导致 Duplication 接口的帧队列满了,后续采集全部阻塞。 解决:确保每次AcquireNextFrame成功后,无论后续处理是否成功,都要在 finally 块里ReleaseFrame。这是血泪经验,少一行就崩。
4.3 音画不同步,越录越偏
现象:开头同步,录到后面人声比画面快或者慢。 原因:音频和视频用了各自独立的时钟,没有做漂移补偿。声卡的实际采样率和声明值有微小偏差,长时间累积就明显了。 解决:以音频时钟为主时钟,视频帧根据采集时刻动态调整 pts,超出阈值就丢帧或重复帧。不要用系统时间做基准,系统时间会被 NTP 调整。
4.4 编码器初始化失败
现象:程序启动报错,提示Failed to open encoder或No capable devices found。 原因:硬编需要特定的 GPU 支持,NVENC 要求 NVIDIA 显卡且驱动版本够新,QSV 要求 Intel 核显且驱动正确安装。如果源码默认走硬编但机器不支持,就会失败。 解决:在源码里加一个编码器回退逻辑,硬编失败自动切软编。或者启动时枚举可用的编码器,让用户选。
4.5 文件体积异常大
现象:录 1 分钟 1080p 视频,文件好几个 GB。 原因:编码器没生效,写进去的是原始帧;或者码率设成了无损,CRF 给了 0。 解决:检查编码器初始化是否成功,ffprobe 看视频流的 codec_name 是不是 h264/hevc。如果是 rawvideo,说明编码环节被跳过了。CRF 建议 18-23,不要给 0。
5. 进阶技巧:用环形缓冲和分段录制做长时间稳定录制
长时间录制是屏幕录像程序源代码里最能体现工程功力的场景。录几个小时甚至几天,最怕的是内存涨、文件坏、磁盘满。我一般会加两个机制:环形缓冲和分段录制。
环形缓冲解决的是编码抖动。采集线程往一个固定大小的帧池里写,编码线程从池里取,池满了就覆盖最旧的帧。这样即使编码偶尔卡顿,也不会无限堆积内存。帧池的大小按fps * 缓冲秒数算,一般给 2-3 秒就够。
分段录制解决的是文件损坏和磁盘管理。不要把所有数据写进一个 MP4,而是每 5 分钟或者每 500MB 切一个文件。切的时候先写完当前文件的 moov,再开新文件。这样即使程序崩溃,最多丢最后一段,前面的都能播。MKV 本身容错好,但分段后管理更清晰。
# 分段录制的简化逻辑 class SegmentWriter: def __init__(self, base_path, segment_duration=300, max_size_mb=500): self.base_path = base_path self.segment_duration = segment_duration # 秒 self.max_size_mb = max_size_mb self.current_file = None self.segment_start = 0 self.segment_index = 0 def write_packet(self, packet, current_time): # 判断是否需要切分 need_split = False if current_time - self.segment_start >= self.segment_duration: need_split = True if self.current_file and self.current_file.size_mb() >= self.max_size_mb: need_split = True if need_split: self.close_current() self.open_next(current_time) self.current_file.write(packet) def close_current(self): if self.current_file: self.current_file.finalize() # 写完 moov self.current_file = Nonesegment_duration=300是 5 分钟,max_size_mb=500是单文件上限,两个条件谁先到就切。finalize这一步不能省,它负责把 MP4 的索引写完整,少了这步文件就是坏的。切分的时候要注意关键帧对齐,最好在 I 帧处切,否则新文件开头会花屏。实际工程里还会在文件名里带上时间戳和序号,方便后续拼接。
验证分段录制是否可靠,可以故意在录制中途 kill 进程,然后检查已完成的段能不能正常播放。如果能播,说明 finalize 逻辑没问题;如果最后一段坏了但前面的都好,那也符合预期。这个习惯我保持了很长时间:任何长时间运行的程序,都要先想好崩溃时怎么收场。希望帮到你。
本文还有配套的精品资源,点击获取