☰
用Qt做Windows屏幕录制:抓屏API选型与FFmpeg编码实践
2026/10/22 10:54:22 网站建设 项目流程

简介:这份源码是一个基于Qt框架开发的Windows屏幕录制工具,面向需要实现或学习桌面录屏功能的Qt开发者。工具支持1080P最高24帧录制、自选屏幕区域、画质与帧率调节,并通过多线程分离截屏与文件写入提升性能,还具备按时间自动分割AVI文件的能力。工程共包含12个文件,其中5个cpp为逻辑实现,4个h为接口声明,另含1个ui界面文件、1个pro工程配置和1个user文件,整体压缩包仅26KB,结构精简易于阅读。目前已有3407人学习下载,适合作为入门级录屏项目的参考模板。通过分析源码可掌握利用Windows设备DC高效抓屏、多线程协作保存数据以及AVI封装格式的基本思路,便于在此基础上扩展录音、硬件加速或推流等功能。

1. 用Qt做Windows屏幕录制,先想清楚这三件事

前些天接了个需求:给内部客服系统做一个操作回放录制模块,要求录制坐席端的操作画面,生成MP4供质检回看。第一时间想到用Qt做,因为整个客户端就是Qt的,加个录制功能不想再引一套跨语言方案。但真正动手才发现,Qt只是外壳,屏幕录制真正的难点全在Windows抓屏和编码这两件事上。如果你也想在Qt应用里集成录屏,先想清楚三点:抓屏用哪条API、编码交给谁、音频走哪条路。选错了,项目的后半段基本就是跟黑屏、卡帧和音画不同步搏斗。这篇文章把三条路、最小实现和踩过的坑一次讲透。适合要在自己Qt项目里集成录屏的开发者,也适合想快速交付一个可用录制小工具的团队。

2. 抓屏路径先选定:GDI、DXGI 还是 Qt 的 QScreenCapture

2.1 三条抓屏路线对比:看起来都能截屏,底层差别很大

Qt里很多人第一反应是用QScreen::grabWindow(0),一行代码就能拿到全屏QImage,简单到让人误以为录屏也这么简单。这个方法在Windows上走的是GDI的BitBlt,从屏幕DC里把像素拷出来。它的优点是兼容性好——Windows 7到Windows 11都能跑,代码量几乎为零;缺点是毛病也最明显:抓不到硬件加速的内容。比如全屏播放视频、游戏画面、部分绘图程序用了GPU渲染的覆盖层,GDI拿到的经常是黑块或者残缺的旧画面。这个在录制浏览器或Office软件时看不出来,一旦录到视频播放器就立刻翻车。

第二条路是DXGI Desktop Duplication,Windows 8开始提供的桌面复制接口。它直接从显卡的桌面副本里拿帧,GDI抓不到的内容它能抓到,而且效率高得多,适合60帧以上的录制。代价是API使用门槛高,你得处理COM接口、纹理拷贝、超时等待,还要应对DXGI_ERROR_ACCESS_LOST这种需要重新初始化的异常状态。Qt没有原生封装这条路线,需要自己用C++写接口层,再把DXGI纹理转成QImage送进编码器。

第三条路是Windows 10 1803开始引入的新一代屏幕捕获接口,Qt 6.5在Windows上的QScreenCapture就是基于它。它的特点是性能好、支持多显示器、还能单独控制是否捕获鼠标光标,而且不需要像DXGI那样独占桌面副本。问题是它只能在Windows 10较新版本上跑,系统太旧就没有对应的运行时接口,Qt这边封装也比较晚,很多生产项目还在用Qt 5.x,用不上。

三选一怎么定,我给一个务实的分法:只录普通桌面软件界面、对帧率要求不高,直接用grabWindow,先把功能跑通;要录视频播放、游戏画面,或者帧率要求超过30fps,直接上DXGI,别在GDI上浪费时间;项目已经用了Qt 6.5以上、目标系统都是Win10 1803以上,优先试QScreenCapture,代码量最少。下表是我实际对比后的结论:

方案最低系统能否抓硬件加速画面开发成本适合场景
QScreen::grabWindow (GDI)Win7否极低录普通窗口、办公软件、操作演示
DXGI Desktop DuplicationWin8能高游戏录制、高帧率、内容安全要求高
QScreenCapture (Win10 1803+)Win10 1803+能低新系统为主、Qt 6.5+项目

2.2 编码交给谁:不建议把希望全押在Qt自带编码器上

抓屏解决的是“拿到像素”,接下来要把一帧帧像素压缩成视频文件。Qt Multimedia的QMediaRecorder在Windows上走Media Foundation,理论上可以封装成MP4,但实际用过的都知道文本地不对,某次在客户Win10机器上跑,系统装了不少精简版,H.264编码器在Media Foundation里直接找不到,程序报错退出,最后只能在代码里写死让用户去装“微软的媒体扩展包”,体验很差。

所以常见的工程做法是:Qt只做采集,编码和封装交给FFmpeg。FFmpeg在Windows下的编码器是自带的(libx264),不依赖系统组件,到哪里行为一致。集成方式有两种,一是链接FFmpeg的库文件,在代码里调avcodec/avformat接口;二是用QProcess启动独立的ffmpeg进程,通过管道喂裸帧数据。前者功能最全、性能最好,但要引入一堆头文件和链接配置;后者实现最快、逻辑直观,编码那些脏活全在进程里,而且ffmpeg命令行本身就非常灵活,改参数只动字符串。我的习惯是先走管道方案验证整条链路,性能不够再换库集成。

2.3 动手验证:你的环境在GDI下能不能拿到完整画面

在写一堆代码之前,先用一个十几行的Qt小程序确认当前环境的DPI缩放和抓屏尺寸是否符合预期。这个验证很有必要,很多录出来发虚、只录到一半屏幕的问题,根源都在DPI处理上。

#include <QGuiApplication> #include <QScreen> #include <QDebug> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QScreen* screen = QGuiApplication::primaryScreen(); QImage frame = screen->grabWindow(0).toImage(); qDebug() << "screen name:" << screen->name(); qDebug() << "logical size:" << screen->size(); qDebug() << "physical size:" << frame.size(); qDebug() << "devicePixelRatio:" << screen->devicePixelRatio(); return 0; }

这段代码会打印当前主屏的逻辑尺寸、抓到的图像物理尺寸和缩放比。如果physical size恰好等于logical size乘以devicePixelRatio,说明系统DPI感知生效了,后续录屏可以按物理尺寸传给编码器。如果两者一样大,说明你的程序跑在系统缩放兼容模式下,截屏区域和实际显示区域对不上,录出来的视频不是偏了就是模糊。这是Windows下录屏第一个要确认的点,很多人忽略它,等录完看回放才发现画面只有左上角一块。

FFmpeg管道的可行性也可以顺手验证。先启动一个ffmpeg进程,从stdin读rawvideo写测试文件,再用Qt往进程的标准输入写一帧数据。如果进程能正常结束并生成可播放的MP4,管线就是通的;如果直接报错,多半是像素格式或尺寸参数不对,这在后文会详细讲。把这一步当成冒烟测试,比写完一整套代码再联调省时间得多。

3. 最小可用的录制器:QTimer抓帧 + FFmpeg管道编码

3.1 核心循环:用QTimer驱动抓帧,注意别在主线程里卡界面

管道方案的最小结构很简单:一个QTimer定时抓屏,一个QProcess启动ffmpeg,抓到的QImage转换成裸RGB数据写进ffmpeg的stdin。下面是一个最小可用的录制类,把视频部分先跑起来,音频和多屏在第4章补。

// ScreenRecorder.h #pragma once #include <QObject> #include <QScreen> #include <QTimer> #include <QProcess> class ScreenRecorder : public QObject { Q_OBJECT public: explicit ScreenRecorder(QScreen* screen, quint32 fps = 30, QObject* parent = nullptr); void start(const QString& filePath); void stop(); private slots: void grabFrame(); private: QScreen* m_screen; QTimer* m_timer; QProcess* m_ffmpeg; quint32 m_fps; bool m_recording = false; };
// ScreenRecorder.cpp #include "ScreenRecorder.h" #include <QImage> #include <QByteArray> #include <QDebug> ScreenRecorder::ScreenRecorder(QScreen* screen, quint32 fps, QObject* parent) : QObject(parent), m_screen(screen), m_fps(fps) { m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &ScreenRecorder::grabFrame); m_ffmpeg = new QProcess(this); connect(m_ffmpeg, &QProcess::readyReadStandardError, this, [this]() { qDebug() << m_ffmpeg->readAllStandardError(); }); } void ScreenRecorder::start(const QString& filePath) { if (m_recording) return; const QSize size = m_screen->size() * m_screen->devicePixelRatio(); QStringList args; args << "-y" << "-f" << "rawvideo" << "-pix_fmt" << "bgr24" << "-s" << QString("%1x%2").arg(size.width()).arg(size.height()) << "-r" << QString::number(m_fps) << "-i" << "-" << "-c:v" << "libx264" << "-preset" << "ultrafast" << "-crf" << "23" << "-pix_fmt" << "yuv420p" << filePath; m_ffmpeg->start("ffmpeg", args); m_timer->start(1000 / m_fps); m_recording = true; } void ScreenRecorder::stop() { if (!m_recording) return; m_timer->stop(); m_ffmpeg->closeWriteChannel(); m_ffmpeg->waitForFinished(3000); m_recording = false; } void ScreenRecorder::grabFrame() { if (!m_recording) return; QImage frame = m_screen->grabWindow(0).toImage(); frame = frame.convertToFormat(QImage::Format_RGB888); // QImage 每行末尾有字节对齐,不能直接整体写入 QByteArray buffer; buffer.reserve(frame.sizeInBytes()); for (int y = 0; y < frame.height(); ++y) { buffer.append(reinterpret_cast<const char*>(frame.constScanLine(y)), frame.bytesPerLine()); } m_ffmpeg->write(buffer); }

代码逻辑说明:抓帧拿到的是QImage,格式不一定是RGB888,所以要显式convertToFormat。转成RGB888后,QImage在内存里的实际顺序是BGR(低位字节是蓝色),所以ffmpeg那边用bgr24对应。抓到的图像尺寸是物理像素,也就是屏幕逻辑尺寸乘以DPR,传给ffmpeg的-s参数必须和这个尺寸一致,否则ffmpeg按错误尺寸解析裸数据,画面会直接花掉。

QTimer用的默认精度在Windows下是CoarseTimer,实际触发时间可能在15ms左右波动,对30fps的录制来说偶尔跳一帧还能接受,如果对帧率稳定性要求高,可以在main函数里设置QTimer::setTimerType(Qt::PreciseTimer),代价是略微增加CPU占用。stop的时候先closeWriteChannel让ffmpeg知道输入结束,再等它把编码队列里的帧全部写完,防止MP4文件缺尾巴。

3.2 FFmpeg启动参数逐个拆解:每个参数都是血泪经验换来的

管道方案里ffmpeg命令行把输入输出都规定死了,这里把几个关键参数拆开讲。

-f rawvideo告诉ffmpeg输入是裸视频流,没有容器封装,必须配合-pix_fmt bgr24、-s和-r三个参数才能正确解析。-s是图像宽高,必须和QImage尺寸一致;-r是输入帧率,建议按实际喂帧频率填,不要为省事写60,实际只喂30帧,ffmpeg会把缺失的帧重复输出,或者产生奇怪的时间戳。

-preset ultrafast是编码速度优先,牺牲压缩率来保证实时性。录屏场景CPU资源本来就紧张,用ultrafast把编码时间压到最低。-crf 23是质量参数,范围0到51,数字越小质量越高文件越大。日常录屏23够用,追求画质可以降到18,但编码压力会大不少。注意-crf和-preset是配合使用的,只调一个另一个不变,画质和体积变化不明显。

输出的-pix_fmt yuv420p是为了兼容性:H.264常见的播放器都认yuv420p,如果直接输出编码器默认的yuv444,很多播放器打开是黑屏。这个属于那种“查半天文档才找到”的坑,建议一开始就写上。

// 常用参数组合速查表 // 高画质离线录制 args << "-preset" << "medium" << "-crf" << "18"; // 实时在线录制,CPU紧张 args << "-preset" << "ultrafast" << "-crf" << "23"; // 体积优先,不追求画质 args << "-preset" << "veryfast" << "-crf" << "28";

3.3 QImage行对齐问题:直接write整个buffer会翻车

QImage在内存里不是严密连续排列的,每行像素数据末尾通常有额外的填充字节,保证每行起始地址按4字节对齐。frame.sizeInBytes()返回的是包含这些填充的总大小,直接m_ffmpeg->write(frame.bits(), frame.sizeInBytes())会把填充字节当成像素数据发给ffmpeg,结果是画面右边缘出现一条斜向的彩色条纹,越靠下偏移越明显,看起来像整幅画面被斜切了。

解决方案就是代码里那个逐行拷贝循环:按constScanLine(y)取每一行的起始地址,只取bytesPerLine()的有效数据,把填充字节过滤掉。这个坑尤其在Qt 5.12以上的版本容易出现,因为不同系统、不同像素格式下bytesPerLine的值都不一样,只在特定环境验证过就发布的项目最容易踩。

// 对比:错误写法 vs 正确写法 // 错误:会带入行尾对齐字节 m_ffmpeg->write(reinterpret_cast<const char*>(frame.bits()), frame.sizeInBytes()); // 正确:逐行拷贝,去掉填充 QByteArray buffer; buffer.reserve(frame.height() * frame.bytesPerLine()); for (int y = 0; y < frame.height(); ++y) { buffer.append(reinterpret_cast<const char*>(frame.constScanLine(y)), frame.bytesPerLine()); }

还有一个容易忽略的性能点:QByteArray在频繁append时可能多次重新分配内存,上面的代码用reserve(frame.sizeInBytes())预先分配足够空间,避免每次抓帧都在堆上动态扩容。如果录4K屏,一帧裸数据就有1920x1080x3约6MB,30fps就是每秒180MB的数据量,内存分配效率直接影响帧率稳定性。这段代码跑通之后,你就拥有了一个能录视频的骨架,接下来补音频、光标和多屏适配。

4. 补上音频、光标与多屏:从"能录"到"能交付"

4.1 录系统声音用WASAPI loopback,麦克风用QAudioSource

视频轨道稳定之后,音频几乎是立刻就会提的需求。如果录的是操作演示,一般只需要麦克风;如果要录带系统提示音的场景,就需要把电脑播放的声音也录进去。Qt的QAudioSource可以直接采集麦克风,但系统声音它无能为力,因为系统声音属于渲染设备,Qt的音频输入接口不区分渲染和采集。Windows上录音频输出设备的通用做法是WASAPI的loopback模式,它允许程序从默认渲染设备上读取播放中的音频流。

麦克风采集代码用QAudioSource就能搞定,参数设置核心是采样率和位深度,需要与后续编码器匹配。

// 麦克风采集:44.1kHz、双声道、16位PCM,喂给ffmpeg的标准配置 QAudioFormat format; format.setSampleRate(44100); format.setChannelCount(2); format.setSampleFormat(QAudioFormat::Int16); QAudioSource* audioSource = new QAudioSource(format, this); audioSource->start(audioDevice); // audioDevice 是一个 QIODevice,可接到QProcess

注意QAudioSource的格式需要先查询设备支持的格式,有些声卡驱动对44100双声道16位支持不佳,实际会出现采集到杂音或设备无法打开的情况。稳妥做法是先枚举QAudioDevice的supportedSampleFormats,从中挑一个与目标格式最接近的。

系统声音的loopback采集需要直接调COM接口,完整代码很长,核心骨架如下:

#include <mmdeviceapi.h> #include <audioclient.h> // 初始化COM(放在录制开始前,只能初始化一次) CoInitializeEx(nullptr, COINIT_MULTITHREADED); IMMDeviceEnumerator* enumerator = nullptr; CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&enumerator)); IMMDevice* device = nullptr; enumerator->GetDefaultAudioEndpoint(eRender, eConsole, nullptr, &device); IAudioClient* audioClient = nullptr; device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&audioClient)); // 获取混音格式,loopback模式下用系统格式最稳 WAVEFORMATEX* mixFormat = nullptr; audioClient->GetMixFormat(&mixFormat); // 以共享模式初始化,启用loopback audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, 0, 0, mixFormat, nullptr); IAudioCaptureClient* captureClient = nullptr; audioClient->GetService(IID_PPV_ARGS(&captureClient)); audioClient->Start(); // 采集循环:按包读取音频数据,写入音频文件或管道 UINT32 packetSize = 0; while (recording) { captureClient->GetNextPacketSize(&packetSize); while (packetSize > 0) { BYTE* data = nullptr; UINT32 frames = 0; DWORD flags = 0; captureClient->GetBuffer(&data, &frames, &flags, nullptr, nullptr); if (!(flags & AUDCLNT_BUFFERFLAGS_SILENT) && frames > 0) { // 把 data 写入音频设备文件,frames * mixFormat->nBlockAlign 为字节数 } captureClient->ReleaseBuffer(frames); captureClient->GetNextPacketSize(&packetSize); } Sleep(10); // 避免空转占用CPU }

这段代码是WASAPI loopback的标准流程,步骤是固定的:拿到枚举器、取默认渲染设备、激活IAudioClient、初始化时带上AUDCLNT_STREAMFLAGS_LOOPBACK标志、从GetService拿IAudioCaptureClient,然后循环取数。注意必须在类初始化时执行一次CoInitializeEx,录制结束别忘了audioClient->Stop()和CoUninitialize(),漏掉会导致程序退出时崩溃。

4.2 光标捕获:抓屏默认不带鼠标,得自己画上去

QScreen::grabWindow拿到的画面里没有鼠标光标,这对录操作演示来说难以接受。解决思路是在抓帧完成后把光标绘制到QImage上。用Qt自身的接口可以做到,但要注意坐标单位:QCursor::pos()返回的是逻辑坐标,在100%缩放下和物理坐标一致,在缩放比例不是100%的机器上需要转换。

// 在抓帧后、写管道前调用,把当前鼠标画到帧上 void paintCursor(QImage& frame, QScreen* screen) { const QCursor cursor = QCursor(); if (cursor.shape() == Qt::BlankCursor) return; // 系统隐藏了光标 QPixmap pixmap = cursor.pixmap(); if (pixmap.isNull()) return; QPoint pos = QCursor::pos(); QPoint hotSpot = cursor.hotSpot(); // 高DPI下,逻辑坐标需要乘DPR才是物理像素坐标 const qreal dpr = screen->devicePixelRatio(); QPainter painter(&frame); painter.drawPixmap((pos - hotSpot) * dpr, pixmap); }

这段代码先说清楚适用边界:适合100%缩放的机器,或者先验证了DPI后再用。在Windows缩放125%或150%的机器上,QCursor::pos()返回的是逻辑坐标,而frame是物理像素尺寸,直接绘制光标位置会偏。工程做法是先判断dpr是否为1,非1时要么按比例换算坐标,要么干脆改用系统API取物理坐标。光标还有一个玄学问题:抓帧完成到绘制光标之间有几毫秒延迟,鼠标快速移动时光标位置和实际画面会错位一小段距离。这对操作演示类录制影响不大,但如果录的是精细的鼠标操作,建议降低抓帧间隔或者用鼠标钩子事件记录更精确的位置。

4.3 多显示器与分辨率:录制指定屏幕,换分辨率不崩溃

多显示器环境下,QGuiApplication::screens()返回所有屏幕,每个屏幕的size()和devicePixelRatio()可能各不相同。录制指定屏幕要靠QScreen指针区分,不能在代码里写死主屏。

// 按索引选屏,越界回退到主屏 QScreen* pickScreen(int index) { const QList<QScreen*> screens = QGuiApplication::screens(); if (index >= 0 && index < screens.size()) { return screens.at(index); } return QGuiApplication::primaryScreen(); }

分辨率变化是录制过程中另一个容易崩的点。用grabWindow抓帧时,如果用户在录制过程中把分辨率从1920x1080改到1280x720,抓到的QImage尺寸立刻改变,但ffmpeg进程的-s参数还停留在1920x1080,写入的数据长度也和预期不一致,轻则画面花掉,重则ffmpeg直接报错退出。要兜住这个场景,最简单的方式是监听屏幕信号并在分辨率变化时手动重启录制进程:

connect(m_screen, &QScreen::geometryChanged, this, [this](const QRect& geo) { if (m_recording) { // 记录当前已录时长,停止当前进程,再用新尺寸重新启动 restartWithNewSize(); } });

实际项目中更稳妥的做法是禁止在录制过程中修改分辨率,或者检测到变化后立即停止录制并给出提示。毕竟录到一半重启编码器,会在MP4里留下一个时间断层,比直接退出更难向用户解释。

5. Windows屏幕录制排障手册:黑屏、卡帧与音画不同步

5.1 录出来只有壁纸和桌面图标,窗口内容全是黑的

现象:用grabWindow录制了一个正在全屏播放视频的窗口,回放时视频区域是黑块,壁纸和任务栏正常;或者UAC弹窗确认的瞬间,全程录到的是黑屏。

原因有两个。第一个是硬件加速:视频播放器、游戏、部分绘图软件用GPU渲染覆盖层,画面不经过GDI,grabWindow根本读不到这部分内容。第二个是UAC安全桌面:Windows在弹出UAC确认窗口时会切换到安全桌面,普通进程无法访问那里的像素。GDI在安全桌面上抓到的是黑屏或者冻结画面。

解决:要么接受现状,在需求层面明确“本方案只能录制普通桌面程序”;要么换DXGI Desktop Duplication方案,它能从显卡的桌面副本直接取帧,硬件加速画面和安全桌面之外的场景都能覆盖。UAC弹窗本身是安全机制,任何录屏软件都无法在弹窗出现时录到内容,工程上的处理是检测到UAC弹窗出现时自动暂停录制并显示提示。检测方式用grabWindow抓到黑帧后统计像素亮度,超过一定比例的纯黑帧就触发暂停逻辑。

5.2 帧率设置30,录出来只有十几帧,画面一顿一顿

现象:QTimer间隔设了33ms,任务管理器里看到进程CPU占用很高,但回放的MP4明显卡,用播放器看统计信息只有每秒15帧左右。

原因:一是QTimer默认精度不够,Windows把计时器分组管理,33ms的间隔可能被调整到48ms;二是grabWindow本身是同步操作,耗时可能达到20ms以上,和定时器的33ms叠加,实际周期变成50ms以上;三是写入管道时QProcess的缓冲区满了会阻塞,进一步拖慢循环。

解决:先用QElapsedTimer统计实际抓帧间隔,排除计时器问题;再把同步抓帧放到子线程,主线程只负责接收QImage并写入管道;最后监控m_ffmpeg->bytesToWrite(),超过阈值说明编码跟不上,主动丢掉当前帧而不是继续堆积。

// 管道积压控制:丢掉来不及编码的帧,保证实时性 if (m_ffmpeg->bytesToWrite() > 64 * 1024 * 1024) { // 积压超过64MB,说明编码速度跟不上抓帧速度 return; }

丢帧策略需要说明:录屏场景丢帧远比延迟好,观看者能接受偶尔跳一帧,但不能接受画面越拉越远。64MB是经验值,具体看系统内存和编码器性能,机器好可以放宽到128MB,配置低就收紧到32MB。

5.3 音画不同步:越录到后面声音越迟

现象:前面30秒音画同步,1分钟后声音落后画面200ms以上,10分钟的录制落后接近2秒。

原因:视频帧的节奏由QTimer驱动,音频的节奏由声卡硬件时钟驱动,两个时钟源本身就存在微小漂移。再加上编码器处理视频需要时间,音频数据按固定速率进入编码器,两边的延迟会不断累积。

解决:最简单可靠的方式是录制阶段把视频和音频分别存到两个文件,结束录制后用ffmpeg做一次合并,用-shortest让输出在较短的那个流结束时收尾,内部会自动对齐时间戳。

ffmpeg -i video.mp4 -i audio.wav -c:v copy -c:a aac -shortest final.mp4

如果坚持录制过程中实时混流,需要把音频时间戳对齐到视频的时钟基准上,这个复杂度和收益不成正比,日常项目不值得。

5.4 录制中拔掉外接显示器,程序直接崩溃

现象:笔记本外接显示器时录副屏,用户拔掉HDMI线的瞬间,录制程序闪退,或者停止响应。

原因:显示器拔掉后,对应的QScreen对象会被销毁,grabWindow对已销毁的屏幕指针操作会触发空指针访问;DXGI方案则表现为DXGI_ERROR_ACCESS_LOST(实际上是非正常中断的专用错误码),句柄失效。

解决:在录制开始时连接QGuiApplication::screenRemoved信号,屏幕被移除时立刻停止录制并提示用户;DXGI方案需要在捕获循环里检查返回值,遇到DXGI_ERROR_ACCESS_LOST就释放当前IDXGIOutputDuplication对象,等显示器重新枚举后再创建新实例。

// Qt方案:屏幕移除时安全停止 connect(QGuiApplication::instance(), &QGuiApplication::screenRemoved, this, [this](QScreen* removed) { if (m_screen == removed) { stop(); // 提示用户:当前录制已停止,因为目标屏幕被移除 } });

这四条排障记录覆盖了我在实际项目里遇到的大部分问题。顺序是按出现频率排的,第一条GDI抓不到硬件加速内容出现概率最高,如果需求里明确要录游戏或视频,直接跳过grabWindow方案选DXGI,能少走至少一周的弯路。

6. 验证录制结果并压住性能边界:用ffprobe和数据说话

录制功能做完之后,交付前一定要做一次量化验证,别只靠肉眼放一遍就认为完成了。我习惯用ffprobe检查输出文件的关键指标,这一步能发现很多回放时才暴露的问题。

# 查看视频流的编码格式、分辨率、帧率 ffprobe -v error -select_streams v:0 \ -show_entries stream=codec_name,width,height,avg_frame_rate \ -of default=noprint_wrappers=1 final.mp4 # 查看音视频流的时长和总码率 ffprobe -v error -show_entries format=duration,bit_rate \ -of default=noprint_wrappers=1 final.mp4

输出里重点看avg_frame_rate是否接近设定的fps。实际结果比设定值低5%以内正常,低超过10%就要检查是不是抓帧线程堵了。bit_rate能看出实际编码码率,如果crf设为23但码率异常高,说明画面里有大量动态细节(比如在滚屏),这时候需要确认编码preset是否真的生效。

性能压测也值得做:把录制目标从1080p换成4K,看CPU占用和帧率保持情况。我见过不少项目在1080p下跑得好好的,一到4K就疯狂掉帧。原因通常是grabWindow在4K下抓一帧耗时翻倍,管道数据量也翻倍。这里给出一个粗略的性能对照关系:1080p30的裸数据流约180MB/s,编码器用ultrafast在主流CPU上能跟上;4K30裸数据约720MB/s,占用内存带宽极大,再叠加编码压力,多数机器会掉到20帧以内。

最后说一条我交付录制模块时的教训:有一回客户反馈“录游戏时画面是黑的”,当时我笃定是客户显卡驱动问题,排查了半天发现就是GDI抓不到硬件加速内容,换成DXGI接口后问题消失。从那以后,凡是录屏需求,我第一句就问“要录的内容里有全屏视频或游戏吗”,有就直接走DXGI,不存侥幸。希望这篇文章能帮你从一开始就选对路,少踩那几个我已经替你踩过的坑。

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

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

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

立即咨询