☰
DirectShow+Qt5的Windows摄像头拍照录像源码解析与避坑指南
2026/10/22 15:47:16 网站建设 项目流程

简介:基于DirectShow与Qt5的Windows摄像头拍照录像软件设计源码,面向具备C++基础、希望深入多媒体采集和桌面应用开发的工程师与爱好者。工程实现了摄像头实时预览、静态拍照、视频录制等完整链路,可支撑课程设计或商业项目二次开发。压缩包约11MB,共415个文件,包含155个头文件、140个C源文件、8个C++源文件以及工程配置、脚本、图标、PDF和CHM帮助文档等。头文件用于声明类和接口,源文件承载采集、编码、界面交互、日志记录等核心逻辑,整体目录划分清晰,便于按模块阅读。视频设备枚举、帧格式转换、文件编码保存等关键环节在源码中均有对应实现,能让读者直观理解DirectShow在设备操作与数据处理上的作用,也能学习如何用Qt5构建稳定的操作界面,适用于Windows各主流桌面环境,部署简单、便于快速验证效果。目前已有402人学习,对需要实际项目参考的开发者来说,是一份兼具实用性和参考价值的工程模板。

1. 基于 DirectShow 和 Qt5 的 Windows 摄像头拍照录像:这套源码组合到底解决了什么

先说一个反直觉的结论:DirectShow 是 Windows 上最老的一代采集框架,Qt5 是跨平台 UI 框架,单看技术选型对比,这套组合好像早该被淘汰了。可真要在 Windows 上做摄像头拍照录像软件,我从实际项目经验看,DirectShow + Qt5 反而是落地最快、踩坑最不玄学的方案之一:设备兼容问题少、错误信息直白、链路结构能可视化检查,源码拆给别人维护也容易。

这个标题描述的是这样一套源码设计:用 DirectShow 负责 Windows 摄像头设备枚举、预览、拍照截帧和录像封装,用 Qt5 负责窗口、按钮、文件路径和状态显示。它和调用系统相机程序的方案不同,采集、编码、保存全部发生在自己进程里,是真正可以二次开发的桌面工具,而不是套壳。

适合谁读:刚接触 DirectShow 但想快速出画面的新手、要维护老旧采集代码的工程师、实验室或产线里需要做采集小工具的人。后面几章我会先讲工程骨架怎么搭,再给最小可运行链路和源码级代码块,最后是参数表和避坑清单。

2. 选型与工程骨架:为什么 DirectShow + Qt5 各管一半

2.1 选型理由:DirectShow 在 Windows 摄像头采集里的位置

DirectShow 本质是一套 COM 组件化多媒体框架,核心概念是过滤器图(Filter Graph)。摄像头是源过滤器,后面接转换过滤器、渲染过滤器,系统通过管脚(Pin)和媒体类型(Media Type)自动协商连接,匹配不上时会自动插入色彩转换器之类的中间滤镜。这套模型虽然老,但非常契合“采集 → 处理 → 输出”的固定流程。

Media Foundation 是更新的替代方案,异步模型更强,但学习曲线明显更陡:设备枚举、媒体会话、拓扑结构、异步回调各自成一套体系。对一个拍照录像工具来说,DirectShow 的“连好一次就一直跑”的模型反而更省心。再加上大多数 USB 摄像头在 Windows 上依然通过 WDM 驱动暴露给系统设备枚举器,DirectShow 能枚举到的设备名和 Media Foundation 基本是同一批。

所以选 DirectShow,不是因为它新,而是因为它把复杂协商交给了 GraphBuilder,我们只需要关心业务:哪个设备、什么分辨率、什么时候截帧、什么时候开始录像。

2.2 模块拆分:采集、预览、回调、UI 四层

我做这类工具时会把代码拆成四个职责明确的层,避免 DirectShow 的 COM 代码和 Qt 控件代码混在一起,否则后期换分辨率或者修崩溃问题会非常痛苦。

模块职责关键接口与 Qt5 的交互
采集层枚举设备、建图、控制运行停止IGraphBuilder、ICaptureGraphBuilder2向上层暴露 Open/Close
预览层把视频渲染到指定 HWNDIVideoWindow嵌入 QWidget
帧捕获层回调截帧、缓存最新帧ISampleGrabberCB通过 Qt 信号发出
录制层编码、封装写文件IAMStreamConfig、IFileSinkFilter保存路径由 UI 传入
UI 层设备选择、预览、按钮、状态QComboBox、QPushButton只做事件转发

预览层是 DirectShow 和 Qt5 最容易打架的地方,后面第 3 章会专门讲。帧捕获层必须独立,因为 SampleGrabber 的回调跑在 DirectShow 的工作线程里,直接碰 QWidget 会引发死锁,这个坑会在第 5 章展开说。

2.3 COM 初始化与工程引入 DirectShow 头文件

先在 Qt5 工程文件里把依赖项加好。DirectShow 的头文件是dshow.h和qedit.h,链接库主要是strmiids.lib,里面放着所有系统滤镜的 CLSID。

QT += core gui widgets TEMPLATE = app TARGET = camera_tool CONFIG += c++11 win32 { LIBS += -lstrmiids -lole32 -loleaut32 DEFINES += NOMINMAX } SOURCES += main.cpp MainWindow.cpp CaptureEngine.cpp HEADERS += CaptureEngine.h

NOMINMAX必须定义,否则 Windows 头文件里的min/max宏会污染 Qt 的全局函数,编译期报一堆莫名其妙的错误。qedit.h在新版 Windows SDK 里被标记为废弃,但依然可用,编译时如果报 C4996 警告,忽略或者加#pragma warning(disable : 4996)都行。

COM 初始化放在采集线程的入口处,我习惯用多线程模式,回调触发时更不容易碰到 Apartment 状态问题:

HRESULT InitComForCaptureThread() { HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) { return hr; } return S_OK; }

注意RPC_E_CHANGED_MODE表示当前线程已经被初始化为其他模式,这时候不能重复初始化,直接继续用即可。COM 的初始化必须和释放成对出现,CoUninitialize 要放在线程退出前,否则滤镜的析构可能发生在 COM 已经卸载之后,会出现随机崩溃。

3. DirectShow 最小链路:枚举摄像头、构建过滤器图、嵌入 Qt 窗口

3.1 枚举摄像头设备:从系统设备枚举器拿 FriendlyName

摄像头枚举用系统设备枚举器,先创建ICreateDevEnum,再按视频输入设备类别枚举,每个设备是一个 Moniker。这一步最需要注意的是CreateClassEnumerator返回S_FALSE表示没有设备,这不是错误,不要当成失败处理。

QStringList EnumerateVideoDevices() { QStringList names; ICreateDevEnum* pDevEnum = nullptr; IEnumMoniker* pEnum = nullptr; HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)&pDevEnum); if (FAILED(hr)) return names; hr = pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnum, 0); if (hr != S_OK) { // S_FALSE 表示没有设备,不是错误 pDevEnum->Release(); return names; } IMoniker* pMoniker = nullptr; while (pEnum->Next(1, &pMoniker, nullptr) == S_OK) { IPropertyBag* pBag = nullptr; hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pBag); if (SUCCEEDED(hr)) { VARIANT var; VariantInit(&var); hr = pBag->Read(L"FriendlyName", &var, nullptr); if (SUCCEEDED(hr)) { names << QString::fromWCharArray(var.bstrVal); VariantClear(&var); } pBag->Release(); } pMoniker->Release(); pMoniker = nullptr; } pEnum->Release(); pDevEnum->Release(); return names; }

FriendlyName是驱动提供的设备名称,不一定和设备管理器里完全一致,但用来给人看足够了。要注意枚举成功不意味着设备能打开,设备可能被别的进程占用,也可能刚插上驱动还没就绪,所以打开失败时 UI 上要允许重新枚举。

3.2 构建预览链路:CaptureGraphBuilder2 一行建图

构建过滤器图我强烈建议用ICaptureGraphBuilder2的RenderStream,而不是自己手动找管脚、手动连接。前者会自动挑选合适的管脚、自动插入 Smart Tee、自动接上视频渲染器,少写几十行容易出错的代码。

HRESULT BuildPreviewGraph(IMoniker* pMoniker, IGraphBuilder** ppGraph, IMediaControl** ppControl, ISampleGrabber** ppGrabber, IVideoWindow** ppVideoWindow) { HRESULT hr = CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)ppGraph); if (FAILED(hr)) return hr; ICaptureGraphBuilder2* pBuilder = nullptr; hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, nullptr, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)&pBuilder); if (FAILED(hr)) return hr; pBuilder->SetFiltergraph(*ppGraph); // 把设备 Moniker 绑定成源过滤器,加入图中 IBaseFilter* pSrc = nullptr; hr = pMoniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, (void**)&pSrc); if (FAILED(hr)) return hr; hr = (*ppGraph)->AddFilter(pSrc, L"VideoSource"); // 中间插一个 SampleGrabber,作为截帧卡口 hr = CoCreateInstance(CLSID_SampleGrabber, nullptr, CLSCTX_INPROC_SERVER, IID_ISampleGrabber, (void**)ppGrabber); if (FAILED(hr)) return hr; AM_MEDIA_TYPE mt; ZeroMemory(&mt, sizeof(mt)); mt.majortype = MEDIATYPE_Video; mt.subtype = MEDIASUBTYPE_RGB24; // 尽量让系统自动转 RGB (*ppGrabber)->SetMediaType(&mt); IBaseFilter* pGrabFlt = nullptr; (*ppGrabber)->QueryInterface(IID_IBaseFilter, (void**)&pGrabFlt); (*ppGraph)->AddFilter(pGrabFlt, L"FrameGrabber"); // 一行建预览链:源 -> SampleGrabber -> 视频渲染器 hr = pBuilder->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pSrc, pGrabFlt, nullptr); // 拿到窗口控制接口,稍后交给 Qt 的 QWidget pBuilder->FindInterface(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pSrc, IID_IVideoWindow, (void**)ppVideoWindow); (*ppGraph)->QueryInterface(IID_IMediaControl, (void**)ppControl); pSrc->Release(); pGrabFlt->Release(); pBuilder->Release(); return hr; }

RenderStream的第四个参数是中间过滤器,这里传 SampleGrabber,它就会自动出现在源和渲染器之间,既不影响预览画面,又能让我们后面截帧。SampleGrabber的媒体类型设置为 RGB24,大多数情况下 GraphBuilder 会尝试插入色彩空间转换器;如果摄像头驱动不支持 RGB24 输出且系统没有合适的转换器,这一步会返回连接失败,那就需要改成枚举驱动能力格式,这个在第 4 章给方案。

3.3 视频窗口嵌入 Qt5:winId、IVideoWindow、高 DPI

DirectShow 的视频渲染器需要一个窗口句柄,而 Qt5 的 QWidget 默认不一定是原生窗口,必须显式调用winId()强制创建,然后把渲染器的 Owner 设置成这个 HWND。

void AttachVideoWindow(IVideoWindow* pVW, QWidget* host) { // 先让 host 变成原生窗口,拿它的 HWND host->setAttribute(Qt::WA_NativeWindow); HWND hwndHost = (HWND)host->winId(); pVW->put_Owner((OAHWND)hwndHost); pVW->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN); pVW->put_Left(0); pVW->put_Top(0); pVW->put_Width(host->width()); pVW->put_Height(host->height()); }

WS_CHILD不能少,少了视频窗口会变成独立顶层窗口,跑到任务栏里。put_Left/Top/Width/Height是让视频画面填满 host 区域,建议在 host 的resizeEvent里重新设置一次,否则窗口拉伸后画面不跟随。

高 DPI 是隐藏的雷。如果程序启用了 Qt 高 DPI 缩放,DirectShow 设置的是物理像素,而 Qt 的host->width()返回的是逻辑像素,两者在缩放率不是 100% 时对不上。我一般干脆不启用 Qt 的高 DPI 缩放,交给 Windows 自己处理;如果必须启用,就在设置宽高时乘以devicePixelRatioF()。

4. 拍照与录像落库:SampleGrabber 截帧、AVI 封装与编码器接入

4.1 拍照:SampleGrabber 回调与 JPG 保存

拍照的核心在 SampleGrabber 的回调类。DirectShow 的工作线程会持续调用BufferCB,我们需要做的是把帧数据拷贝出来,而不是直接在处理函数里写文件。

class FrameCallback : public ISampleGrabberCB { public: STDMETHODIMP_(ULONG) AddRef() { return 1; } STDMETHODIMP_(ULONG) Release() { return 1; } STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { if (riid == IID_IUnknown || riid == IID_ISampleGrabberCB) { *ppv = this; return S_OK; } *ppv = nullptr; return E_NOINTERFACE; } STDMETHODIMP SampleCB(double, IMediaSample*) { return S_OK; } STDMETHODIMP BufferCB(double, BYTE* pBuf, long len) { if (len <= 0 || !pBuf) return S_OK; QMutexLocker locker(&m_mutex); m_data = QByteArray((const char*)pBuf, len); // 必须拷贝 return S_OK; } QImage GrabFrame(int w, int h) { QMutexLocker locker(&m_mutex); if (m_data.isEmpty()) return QImage(); return QImage((const uchar*)m_data.constData(), w, h, w * 3, QImage::Format_RGB888).copy(); } private: QMutex m_mutex; QByteArray m_data; };

回调里的pBuf指向的是 DirectShow 内部的缓冲池,这个缓冲可能被系统复用,所以必须立即拷贝成 QByteArray。QMutex是必需的,因为拍照动作发生在 UI 线程,而BufferCB在采集线程,两者并发访问m_data不加锁会随机崩溃。

拍照时调用GrabFrame(w, h)拿到 QImage,然后img.save(fileName, "JPG", 90)。如果拍出来的图有斜条纹或者颜色错乱,大概率是 stride 对齐问题,也就是每行实际字节数不是width * 3,这种情况我会手动按(w * 3 + 3) & ~3作为每行步长逐行拷贝。另外拍照前最好让回调先跑个几帧,刚开图时第一帧往往是半黑状态。

4.2 录像:AVI 容器、编码器选择与链路构建

录像我推荐走 AVI 容器加 MJPEG 编码器,原因很简单:MJPEG 压缩率高、几乎全平台播放器能解、CPU 开销小,而且 DirectShow 系统里自带 MJPEG Compressor 滤镜,不需要额外安装编解码器。

HRESULT BuildRecordGraph(IGraphBuilder* pGraph, ICaptureGraphBuilder2* pBuilder, IBaseFilter* pSrc, const wchar_t* outFile) { // 指定输出文件,系统会创建 AVI Mux 和 File Writer IFileSinkFilter* pSink = nullptr; IBaseFilter* pMux = nullptr; HRESULT hr = pBuilder->SetOutputFileName(&MEDIASUBTYPE_Avi, outFile, &pSink, &pMux); if (FAILED(hr)) return hr; // 找一个视频压缩滤镜 IBaseFilter* pEnc = FindEncoderByName(L"MJPEG Compressor"); if (!pEnc) return E_FAIL; // 源.Capture -> 编码器 -> AVI Mux -> File Writer hr = pBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pSrc, pEnc, pMux); if (FAILED(hr)) return hr; return S_OK; }

SetOutputFileName的第二个参数是输出路径,路径里如果有中文,注意用宽字符,否则文件创建失败。FindEncoderByName是通过系统设备枚举器遍历视频压缩器类别,按 FriendlyName 匹配,这里我没直接写死 CLSID,是因为不同系统上 MJPEG Compressor 的注册情况略有差异,枚举更稳。

要提醒一点:预览链路已经占用源的情况下,再构建录像链路时,CaptureGraphBuilder2 会自动插入一个 Smart Tee,把源的数据分流成预览和录制两路。这是它的标准行为,不需要我们手动去插。

4.3 录制前必调参数:分辨率、帧率、比特率

分辨率、帧率必须从摄像头能力表里选,不能凭感觉填。摄像头驱动不支持的分辨率,SetFormat 会直接失败。

HRESULT SetVideoFormat(ICaptureGraphBuilder2* pBuilder, IBaseFilter* pSrc, int cx, int cy, double fps) { IAMStreamConfig* pCfg = nullptr; HRESULT hr = pBuilder->FindInterface( &PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pSrc, IID_IAMStreamConfig, (void**)&pCfg); if (FAILED(hr)) return hr; int iCount = 0, iSize = 0; pCfg->GetNumberOfCapabilities(&iCount, &iSize); AM_MEDIA_TYPE* pmt = nullptr; VIDEO_STREAM_CONFIG_CAPS caps; for (int i = 0; i < iCount; i++) { hr = pCfg->GetStreamCaps(i, &pmt, (BYTE*)&caps); if (FAILED(hr)) continue; VIDEOINFOHEADER* vih = (VIDEOINFOHEADER*)pmt->pbFormat; if (vih->bmiHeader.biWidth == cx && vih->bmiHeader.biHeight == cy) { vih->AvgTimePerFrame = (REFERENCE_TIME)(10000000.0 / fps); hr = pCfg->SetFormat(pmt); DeleteMediaType(pmt); break; } DeleteMediaType(pmt); pmt = nullptr; } pCfg->Release(); return hr; }

AvgTimePerFrame的单位是 100 纳秒,所以 30 帧每秒对应的值是10000000.0 / 30。分辨率、帧率这个组合必须严格从GetStreamCaps返回的列表里选,很多摄像头支持 640x480@30,但不支持 640x480@60,强行设置会失败。

参数建议值说明
容器格式MEDIASUBTYPE_Avi兼容性最好,AVI Mux 自动写索引
编码器MJPEG Compressor系统自带,画质和体积均衡
分辨率从能力表枚举不要硬编码,不同摄像头差异大
帧率能力表匹配值预览和录像共用同一路源,改格式要重建图
输出路径宽字符SetOutputFileName 不接受窄字符路径

格式设置必须在建图之前完成,因为预览和录像共用同一个源过滤器的管脚,运行中改格式需要停掉整个图再重建,否则 SetFormat 会返回正在使用中的错误。

5. DirectShow 与 Qt5 联调避坑:黑屏、黑帧、录像失败的 5 个典型现场

5.1 现象 1:预览黑屏,但程序没崩溃

现象:界面正常显示,设备也枚举到了,但视频区域一片黑。

原因:最常见的是图没有 Run。建好过滤器图只完成了连接,必须调用IMediaControl::Run()才开始流转数据。另一个典型原因是IVideoWindow::put_Owner设置了 HWND,但之后 Qt 的重绘把子窗口区域盖住了,或者高 DPI 下设置的宽高和实际窗口大小不一致。

解决:建图后先调用pControl->Run(),确认返回 S_OK;再检查put_WindowStyle是否包含WS_CHILD。在 host 的resizeEvent里重新调用一次put_Width/put_Height,并换算成物理像素。如果是 Qt 合成器的问题,给 host 设置Qt::WA_PaintOnScreen可以缓解,但要注意这会关闭 Qt 对该控件的合成绘制。

5.2 现象 2:拍出来的照片全是黑帧

现象:GrabFrame返回的 QImage 非空,但保存成 JPG 后整张图是黑的。

原因:SampleGrabber 的媒体类型设置成 RGB24,但源过滤器和渲染器连接时并没有真正转换成 RGB24,回调里拿到的缓冲还是原始格式,按 RGB24 解释就变成黑图或者花图。另一个原因是刚开图时立刻拍照,DirectShow 还没来得及把有效帧送到 SampleGrabber。

解决:先让回调跑满 10 帧再允许拍照,UI 上按钮默认禁用,收到帧计数后启用。格式方面,如果回调数据明显不是 RGB24,改成把 SampleGrabber 的媒体类型设置为MEDIASUBTYPE_NULL,让它接受任何视频格式,然后在回调里根据连接时协商出的格式手动转换,或者直接用系统色彩转换器。但注意转换操作不要放在回调里,拷贝原始数据后在拍照线程转换。

5.3 现象 3:录像文件打不开,或者时长不对

现象:点停止录像后,文件大小很小,播放器提示文件损坏;或者播放时画面快进、卡顿。

原因:AVI 文件需要 AVI Mux 在收到 Stop 信号时写索引头,如果程序直接释放图,没有正确停止,Mux 没有机会落索引,文件就是不完整的。时长不对通常是音视频两路流的参考时钟不一致,常见于只录视频不录音频时还强行设置了音频参数。

解决:停止录像时先pControl->Stop(),等待 200 到 500 毫秒,再释放 Mux 和 File Sink 的引用。如果已经用 SetOutputFileName 建了 AVI Mux,就不要手动去移除它的管脚,直接停图即可。需要音画同步时,给图设置参考时钟,最简单的方式是让 CaptureGraphBuilder 自动处理,不要自己手动设置两条流的帧率。

5.4 现象 4:UI 卡死,或者 SampleGrabber 回调里崩溃

现象:窗口能画出来,但一动就卡,有时直接闪退;崩溃堆栈指向回调函数。

原因:SampleGrabber 的BufferCB运行在 DirectShow 的采集线程,在这个线程里创建 QImage、操作 QWidget 或调用QMessageBox,都会和 Qt 的主事件循环抢锁,轻则卡顿,重则死锁闪退。这也是新手最容易犯的错误。

解决:回调里的全部工作就是加锁、拷贝数据、解锁、发一个 Qt 信号。UI 线程通过信号槽拿到 QByteArray 后再做保存、预览、统计。如果拍照和录像同时进行,帧数据可能很大,建议用一个固定容量的环形队列,由独立线程负责落盘,不要在采集线程里做任何 I/O。

5.5 现象 5:插拔摄像头后枚举不到设备,或者枚举到但打不开

现象:程序开着时拔掉 USB 摄像头再插上,点刷新按钮列表为空;或者列表里有名字但打开失败。

原因:系统设备枚举器可能缓存了旧的 Moniker,插拔后设备路径变了,旧的 Moniker 已经失效。更常见的是旧图没有完全释放,源过滤器还占着设备,新实例打开时驱动返回设备忙。

解决:枚举前先彻底关掉旧图,执行 Stop、移除所有过滤器、Release 所有接口,然后再枚举。每次枚举都重新创建枚举器,不要缓存 IEnumMoniker。程序里加一个“刷新设备”按钮,失败时先走一遍完整清理。

6. 用 GraphStudioNext 验证过滤器图:一套能复查的排查习惯

6.1 把运行中的过滤器图的结构打出来

调试阶段我最先做的是把当前图里的过滤器清单打出来,确认链路确实是预期的结构,而不是靠肉眼猜。

void DumpGraph(IGraphBuilder* pGraph) { IEnumFilters* pEnum = nullptr; pGraph->EnumFilters(&pEnum); if (!pEnum) return; IBaseFilter* pFlt = nullptr; while (pEnum->Next(1, &pFlt, nullptr) == S_OK) { FILTER_INFO info; pFlt->QueryFilterInfo(&info); qDebug() << "[Filter]" << QString::fromWCharArray(info.szName); pFlt->Release(); } pEnum->Release(); }

这个函数能告诉你源过滤器后面接了什么、SampleGrabber 是否真的在图里、Video Renderer 有没有出现。配合 GraphStudioNext 工具,把程序运行中的图挂到 Running Object Table 上,就能在外部看到每个管脚的媒体类型,这是查“为什么没画面”“为什么没声音”最直接的路径。我现在的习惯是每改一次链路就先导图确认结构,再继续写功能,等出问题时就不会靠玄学猜。

6.2 用回调统计实时帧率

怀疑掉帧时,千万别凭肉眼判断,直接在 BufferCB 里计数,每秒在 UI 状态栏显示实时帧率。

// 在 FrameCallback 里加一个计数 volatile long frame_count = 0; // BufferCB 中执行 InterlockedIncrement(&frame_count); // UI 线程用 QTimer 每秒读一次 int fps = InterlockedExchange(&frame_count, 0); statusBar()->showMessage(QString("FPS: %1").arg(fps));

帧率稳定但画面卡,问题出在渲染或 UI 刷新;帧率本身就低,问题出在驱动格式或 CPU 编码瓶颈,这一步能快速定位方向。

6.3 清理顺序:先 Stop 再移除过滤器

关图和开图同样重要。我见过太多因为释放顺序错了,导致第二次打开设备失败的案例。正确顺序是:先 Stop,再枚举过滤器逐个从图中移除,最后释放 COM 接口。

void ShutdownGraph(IGraphBuilder* pGraph, IMediaControl* pMC) { if (pMC) pMC->Stop(); IEnumFilters* pEnum = nullptr; pGraph->EnumFilters(&pEnum); if (pEnum) { IBaseFilter* pFlt = nullptr; while (pEnum->Next(1, &pFlt, nullptr) == S_OK) { pGraph->RemoveFilter(pFlt); pFlt->Release(); } pEnum->Release(); } // 等待 DirectShow 内部清理完成 QThread::msleep(200); }

先 Stop 是为了让 AVI Mux 写文件索引,把图里的过滤器全部移除,是为了让源设备彻底释放,这样下次枚举打开才不会遇到设备忙。CoUninitialize 放在采集线程退出之后再调用,整个生命周期保持对称。

这套 DirectShow + Qt5 的组合,价值不在于技术新旧,而在于它把复杂编码封装在系统滤镜里,把页面呈现交给 Qt,我们真正要维护的只是链路拼接和数据处理。希望这篇笔记里的工程骨架、参数表和避坑清单,能帮你把第一个摄像头工具跑起来,少走几段我走过的弯路。希望帮到你。

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

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

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

立即咨询