尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VS2010 MFC工程DirectShow摄像头采集与视频录制实战详解

VS2010 MFC工程DirectShow摄像头采集与视频录制实战详解 简介这套基于MFC与C的摄像头调用示例工程面向在Visual Studio 2010中编写Windows桌面程序的开发者尤其适合刚接触DirectShow视频采集的新手围绕“打开摄像头并实时预览”演示了从MFC对话框布局到视频流渲染的完整路径。ZIP压缩包共78个文件、约35.71MB包含sln、vcxproj、rc等工程文件cpp/h源码exe可执行程序avi演示视频和bmp示例图片同时带有Debug与Release两套编译产物及obj、pch、pdb、tlog等中间文件方便直接运行和对比编译差异。目前已有1030人浏览学习被不少入门开发者作为摄像头调用练习的参考。通过这份完整工程读者能得到可直接编译运行的MFC对话框程序覆盖新建项目、添加按钮、编写事件到调用DirectShow枚举摄像头、构建过滤器图、启动预览的完整流程还能学习CvvImage辅助类的集成了解VS2010中解决方案、资源脚本、预编译头、编译日志等文件的作用为后续截图、录像等功能打下基础。1. 一个 VS2010 老工程为什么 2025 年还有人在翻拿到这份VideoMFC.zip里面躺着一整套 VS2010 时代遗留下来的工程文件.sln、.vcxproj、.suo、ipch缓存、Debug和Release目录。第一眼看上去像是一堆没人愿意清理的构建垃圾但VideoMFCDlg.cpp、CvvImage.h、MyVideo.avi这几个文件暴露了它的真实身份这是一个基于 MFC 对话框框架、用 DirectShow 做视频采集的最小可用示例。在 OpenCV 的VideoCapture还没像今天这么顺手、UVC 摄像头驱动协议尚未完全统一的时候这种工程就是许多嵌入式视觉和工业上位机项目的起点。这段代码的价值不在于它有多优雅——事实上 h 它连CvvImage这种上古封装都还在用而在于它把 Windows 平台上摄像头采集的完整链路COM 初始化、设备枚举、过滤器图构建、视频预览、帧抓取全部压缩在一个对话框程序里。对正在维护老 MFC 项目、或者需要在 Windows 原生环境里处理摄像头帧数据的开发者来说这个工程比任何现代化的跨平台方案都更贴近底层 Driver 层的行为。2. 认识工程骨架从 .sln 到 VideoMFCDlg 的完整关系2.1 项目文件布局哪些是源码哪些是垃圾解压后的目录结构会让不少新手困惑这里先做一个快速的分类。VideoMFC.sln和VideoMFC.vcxproj是 Visual Studio 2010 的解决方案与项目文件前者是入口后者记录了编译选项、包含路径和源文件清单。resource.h和VideoMFC.rc是 MFC 的资源脚本里面定义了对话框 ID、按钮 ID、图标和版本信息。真正的逻辑代码集中在VideoMFCDlg.cpp/.h对话框主逻辑、VideoMFC.cpp/.h应用入口、CvvImage.cpp/.h图像转换辅助。MyVideo.avi是工程编译后运行过程生成的录像文件恰好说明这个示例除了预览还内置了 AVI 录制功能。其余大量文件如.tlog目录下的编译日志、.pdb调试符号、ipch预编译头缓存、.suo解决方案用户选项全部由编译器自动生成。vc100.idb和vc100.pdb分别是 VS2010 特有的增量编译数据库和程序数据库属于典型的构建过程文件。下载后只保留.sln、.vcxproj、.cpp/.h、.rc和resource.h其余全部可以扔进回收站不影响后续的重新编译。2.2 MFC 对话框程序的消息映射机制VideoMFCDlg继承自CDialogEx这是 MFC 对话框的基类。MFC 和 Win32 SDK 编程最大的区别在于消息映射表机制把 Windows 消息和类成员函数绑定在一起编译器通过BEGIN_MESSAGE_MAP宏展开生成跳转表。BEGIN_MESSAGE_MAP(CVideoMFCDlg, CDialogEx) ON_BN_CLICKED(IDC_BUTTON_OPEN, CVideoMFCDlg::OnBnClickedButtonOpen) ON_BN_CLICKED(IDC_BUTTON_CAPTURE, CVideoMFCDlg::OnBnClickedButtonCapture) ON_WM_TIMER() END_MESSAGE_MAP()ON_BN_CLICKED宏做的事情很直接当对话框上的按钮IDC_BUTTON_OPEN被鼠标点击时框架内部通过DispatchMessage把WM_COMMAND消息路由到OnBnClickedButtonOpen函数。这三个宏对应了打开摄像头、抓拍单帧、定时器刷新三个核心动作。用WM_TIMER而不是单独开线程循环去抓帧是 DirectShow 预览模式下常见的轻量做法定时器每 30ms 触发一次从 SampleGrabber 读取当前帧并更新在 Picture 控件上。这个方案的优点是避免了自己管理线程的生命周期缺点是无法精确控制帧率。2.3 对话框控件的 ID 与资源绑定IDC_BUTTON_OPEN、IDC_BUTTON_CAPTURE、IDC_STATIC_PREVIEW这些 ID 定义在resource.h中。MFC 通过DoDataExchange函数把控件 ID 和成员变量关联起来void CVideoMFCDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_STATIC_PREVIEW, m_staticPreview); }DDX_Control把对话框模板中的静态文本控件绑定到m_staticPreview这个CStatic对象。之后所有对预览画面的操作无论是设置位图还是调整控件尺寸都直接操作这个成员变量。这个绑定发生在OnInitDialog之前的DoDataExchange阶段因此不能在对话框构造函数里访问m_staticPreview这是 MFC 新手最容易触发的断言失败点。3. DirectShow 视频采集COM 初始化到过滤器图构建3.1 为什么 Windows 原生摄像头开发绕不开 DirectShowDirectShow 是微软在 DirectX 家族中负责多媒体流处理的框架。它把采集、解码、渲染、存储抽象为过滤器Filter通过过滤器图Filter Graph把各个阶段串联起来。摄像头在 DirectShow 的视角下就是一个输出MEDIATYPE_Video数据的采集过滤器下游接一个预览渲染器就能看到画面接 SampleGrabber 就能抓帧接 AVI Muxer 就能录像。OpenCV 的VideoCapture在 Windows 后端本质上也是封装了 DirectShow新版走 Media Foundation它把设备枚举、格式协商、帧同步这些脏活包在了黑盒里。直接写 DirectShow 的好处在于你能拿到IAMStreamConfig接口去逐项检查摄像头支持的像素格式YUY2、MJPG、NV12能直接控制VIDEOINFOHEADER里的帧率分母这些在 OpenCV 里并不总是能精细控制。3.2 完整的设备枚举与过滤器图构建代码打开摄像头的核心流程可以拆成五个步骤初始化 COM 环境、枚举系统摄像头设备、创建过滤器图、把采集过滤器加入图、渲染预览流。void CVideoMFCDlg::OnBnClickedButtonOpen() { HRESULT hr CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); if (FAILED(hr)) return; // 1. 创建设备枚举器遍历系统中所有 DirectShow 过滤器类别 ICreateDevEnum* pDevEnum NULL; hr CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (LPVOID*)pDevEnum); if (FAILED(hr)) return; IEnumMoniker* pEnum NULL; hr pDevEnum-CreateClassEnumerator(CLSID_VideoInputDeviceCategory, pEnum, 0); if (hr ! S_OK) return; // S_FALSE 表示没有发现摄像头注意这里不能用 FAILED 判断 // 2. 取出第一个摄像头设备描述符 IMoniker* pMoniker NULL; ULONG cFetched 0; hr pEnum-Next(1, pMoniker, cFetched); if (hr ! S_OK) return; // 3. 通过 Moniker 绑定到真正的 IBaseFilter 采集过滤器 IBaseFilter* pCaptureFilter NULL; hr pMoniker-BindToObject(NULL, NULL, IID_IBaseFilter, (LPVOID*)pCaptureFilter); pMoniker-Release(); // 4. 创建过滤器图管理器构建整个处理管线 hr CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IFilterGraph, (LPVOID*)m_pGraph); m_pGraph-AddFilter(pCaptureFilter, LUSB Camera); // 5. 让过滤器图自动完成采集到渲染的连接 ICaptureGraphBuilder2* pBuilder NULL; CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (LPVOID*)pBuilder); pBuilder-SetFiltergraph(m_pGraph); pBuilder-RenderStream(PIN_CATEGORY_PREVIEW, MEDIATYPE_Video, pCaptureFilter, NULL, NULL); // 6. 启动图表视频流开始流动 IMediaControl* pMediaControl NULL; m_pGraph-QueryInterface(IID_IMediaControl, (LPVOID*)m_pMediaControl); m_pMediaControl-Run(); if (pBuilder) pBuilder-Release(); pCaptureFilter-Release(); pDevEnum-Release(); pEnum-Release(); }这段代码里有三个容易踩坑的细节。第一CreateClassEnumerator成功时的返回值不是S_OK而是S_FALSE如果设备列表为空返回S_FALSE同时pEnum为 NULL。用单纯FAILED(hr)判断会漏掉空设备的情况返回S_FALSE时应该直接退出。第二BindToObject之后pMoniker必须立刻释放否则后续的pEnum-Next会沿用旧的枚举状态。第三RenderStream只指定了PIN_CATEGORY_PREVIEW而不是PIN_CATEGORY_CAPTURE前者的管线末端默认连接视频渲染窗口适合做屏幕预览后者适合录像但默认不显示画面。3.3 线程模型为什么对话框程序不能直接在 UI 线程跑图上面的代码全部运行在 MFC 对话框主线程。m_pMediaControl-Run()返回后DirectShow 内部会创建多个工作线程来拉取摄像头数据并推送给渲染器。渲染器在绘制到窗口时通过PostMessage通知 UI 线程刷新因此不会阻塞消息循环。但是如果在按钮点击处理函数里做了阻塞等待帧数据的操作比如同步抓帧并保存 BMP就会把 UI 线程卡死表现为窗口无响应鼠标变成转圈状态。线程模型的选择建议是万不得已需要在 UI 线程处理帧数据时使用定时器轮询ISampleGrabber的缓冲区和PeekMessage主动让出 CPU要追求高帧率采集就自己开一个工作线程用事件句柄同步帧数据通过PostMessage把帧指针传给主线程绘制。3.4 视频格式协商IAMStreamConfig 与像素格式选择摄像头默认输出的像素格式通常是 YUY2这是一种每像素 16 位的 YUV 4:2:2 格式Windows 渲染器和图像库大多对它做了优化。但 MJPEG 格式在同样带宽下能获得更高的帧率因为传感器内部直接输出压缩 JPEG 帧对 USB 2.0 摄像头尤其明显。IAMStreamConfig* pStreamConfig NULL; pCaptureFilter-QueryInterface(IID_IAMStreamConfig, (LPVOID*)pStreamConfig); int iCount 0, iSize 0; pStreamConfig-GetNumberOfCapabilities(iCount, iSize); if (iSize sizeof(VIDEO_STREAM_CONFIG_CAPS)) { for (int i 0; i iCount; i) { VIDEO_STREAM_CONFIG_CAPS scc; pStreamConfig-GetStreamCaps(i, pmt, (BYTE*)scc); if (pmt-majortype MEDIATYPE_Video pmt-subtype MEDIASUBTYPE_MJPG) { pStreamConfig-SetFormat(pmt); // 切换到 MJPEG 编码 break; } } }GetNumberOfCapabilities返回的是摄像头驱动支持的格式数量GetStreamCaps逐个取出每一种具体格式的媒体类型。切换编码格式的本质是修改AM_MEDIA_TYPE的subtype字段解码为 YUY2 时显示速度快、CPU 占用低解码为 MJPEG 时因为带宽需求小所以可以跑到更高帧率。对实时画面要求高的场景建议选用 MJPEG 配合软件解码但要额外付出 CPU 解码开销。3.5 帧抓取ISampleGrabber 在回调中的内存管理RenderStream建立的预览链路里插入一个 SampleGrabber 过滤器就能在视频流经过时拦截每一帧数据。SampleGrabber 有两种工作模式轮询模式读取缓冲区里最新一帧和回调模式每来一帧触发一次回调函数。轮询模式实现简单但会有帧延迟适合定时器配合使用回调模式实时性强但回调函数运行在 DirectShow 的工作线程中不能直接操作 MFC 控件。// 回调模式需要实现 ISampleGrabberCB 接口 class CSampleGrabberCB : public ISampleGrabberCB { public: STDMETHODIMP_(ULONG) AddRef() { return 1; } STDMETHODIMP_(ULONG) Release() { return 2; } STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { return E_NOTIMPL; } STDMETHODIMP SampleCB(double SampleTime, IMediaSample* pSample) { BYTE* pBuffer NULL; pSample-GetPointer(pBuffer); long lSize pSample-GetActualDataLength(); // 这里不能直接调用 AfxGetApp-GetMainWnd 等 MFC 函数 // 用 PostMessage 把 pBuffer 地址传递到主线程处理 g_hWnd-PostMessage(WM_CAPTURE_FRAME, (WPARAM)pBuffer, lSize); return S_OK; } };回调里最大的坑是IMediaSample指针的生命周期DirectShow 不会在回调返回后自动保留缓冲区下一帧到达时同一块内存可能已被覆盖。安全的做法是在回调里用memcpy把数据拷贝到自己的缓冲区或者直接传递指针并配合主线程的信号量同步。注意WM_CAPTURE_FRAME消息传递的WPARAM是堆内存地址不是有效帧数据——主线程收到消息后应立即拷贝然后释放或者更稳妥的是在回调里申请新内存让主线程负责释放两处互补破坏。4. 从示例代码到可用工具错误处理、逐帧率和录像落地4.1 十行代码教会你分级错误处理而不是靠 Player 崩溃工程里的OnBnClickedButtonOpen函数几乎没有错误处理这在 DOS 窗口里勉强能跑在 MFC 对话框里一遇到无摄像头设备就会崩溃或者黑屏。给 DirectShow 调用加一个逐级返回的检查机制是基本素养。HRESULT hr S_OK; CString strError; #define CHECK_HR(expr) \ do { hr (expr); if (FAILED(hr)) { strError _T(#expr) _T( 失败); goto CleanUp; } } while (0) CHECK_HR(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)); CHECK_HR(pDevEnum-CreateClassEnumerator(CLSID_VideoInputDeviceCategory, pEnum, 0)); if (!pEnum) { strError _T(未检测到摄像头设备); goto CleanUp; } // ...后续步骤逐项检查 CleanUp: if (!strError.IsEmpty()) MessageBox(strError, _T(摄像头错误), MB_ICONERROR);用宏包裹每条调用只是为了精简代码生产环境更推荐封装成独立的函数逐段判断。核心思路是把每一步的HRESULT都当作可决定程序分支的信号而不是等最后pGraph-Run()失败了才去猜哪一环出了问题。尤其CreateClassEnumerator返回S_FALSE但hr仍然等于S_OK的情况必须单独判断pEnum是否为 NULL这是微软文档里含糊其辞、实际工程中必踩的一坑。4.2 定时器与帧率为什么用定时器读不到 30 帧DirectShow 预览模式下用定时器SetTimer(1, 33, NULL)配合OnTimer去抓帧理论上应该拿到近似 30fps但实际经常只有 15 到 18fps。原因在WM_TIMER消息的最低优先级只要消息队列里有鼠标移动、按键、绘制等消息定时器消息就会被压后。用定时器做帧率控制不能直接设 33ms 并指望它精确常规的做法是用QueryPerformanceCounter记录时间戳在定时器里计算出两帧之间的真实间隔并据此动态调整下一次SetTimer的毫秒数。或者干脆放弃定时器在 SampleGrabber 回调里直接统计单位时间帧数作为实际帧率的测量值而不是设定值。4.3 把独立帧保存成 AVIAVI Mux 过滤器与文件写入工程目录下的MyVideo.avi就是运行过程中录制的视频。DirectShow 保存 AVI 的标准做法是把 SampleGrabber 或者采集过滤器的输出接到 AVI Muxer 过滤器再接到 File Writer。IBaseFilter* pMux NULL; IBaseFilter* pFileWriter NULL; CoCreateInstance(CLSID_AviDest, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (LPVOID*)pMux); CoCreateInstance(CLSID_FileWriter, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (LPVOID*)pFileWriter); IFileSinkFilter* pSink NULL; pFileWriter-QueryInterface(IID_IFileSinkFilter, (LPVOID*)pSink); pSink-SetFileName(Lrecord.avi, NULL); m_pGraph-AddFilter(pMux, LAVI Muxer); m_pGraph-AddFilter(pFileWriter, LFile Writer); pBuilder-RenderStream(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, pCaptureFilter, pMux, pFileWriter);这段代码把捕获引脚Capture Pin接到 Muxer 而不是预览引脚Preview Pin。同一个采集过滤器通常有两个输出引脚预览引脚输出到视频渲染器显示捕获引脚输出给后续存储模块。录像用的必须是捕获引脚因为当视频渲染窗口最小化或者被遮挡时DirectShow 会主动丢帧保证实时性而 AVI 录制需要每一帧都完整写入。4.4 资源释放DirectShow 图表的关闭顺序MFC 对话框退出的瞬间直接调用delete m_pGraph或者CoUninitialize()大概率会导致二次崩溃因为 DirectShow 内部线程还在处理视频流。正确的关闭顺序是void CVideoMFCDlg::OnClose() { if (m_pMediaControl) { m_pMediaControl-Stop(); // 1. 停止数据流动暂停所有过滤器 m_pMediaControl-Release(); m_pMediaControl NULL; } if (m_pGraph) { m_pGraph-Release(); // 2. 释放过滤器图管理器 m_pGraph NULL; } CoUninitialize(); // 3. 只有在所有 COM 接口都释放后才能调用 CDialogEx::OnClose(); }Stop()会同步等待所有内部线程退出因此调用后不能立刻在另一个线程访问过滤器图。RUN状态直接释放接口的行为是未定义的多半会引发0xC0000005访问冲突。CoUninitialize的位置也有讲究——如果构造函数里初始化了 COM那么析构里再释放否则只能在OnClose里释放但确保此后不再调用任何 COM 接口函数。5. 老工程现代化改造高帧率先行验证技巧拿到这个工程的第一步不是急着加功能而是先验证摄像头采集链路本身是否通畅。分享一个有效的改造手法把OnBnClickedButtonOpen里的RenderStream目标改成NULL默认渲染到视频窗口然后自己添加一个跟踪窗口标题的辅助代码用标题栏实时显示当前的渲染帧率这样在不改任何 UI 布局的情况下就能直接观察摄像头输出是否稳定。void CVideoMFCDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { static DWORD dwLastTick GetTickCount(); static int nFrameCount 0; nFrameCount; DWORD dwNow GetTickCount(); if (dwNow - dwLastTick 1000) { double fps nFrameCount * 1000.0 / (dwNow - dwLastTick); CString strTitle; strTitle.Format(LVideoMFC - 实时帧率: %.1f fps, fps); SetWindowText(strTitle); nFrameCount 0; dwLastTick dwNow; } } }GetTickCount做统计的精度够用但更高频率的测量建议换成QueryPerformanceCounter。这段代码的价值在于不需要额外仪表直接把帧率反馈到窗口标题便于后续验证分辨率切换、MJPEG 格式切换和IAMStreamConfig参数调整对性能的实际影响。老项目里留下的CvvImage和MyVideo.avi不是摆设CvvImage负责把 DirectShow 输出的 YUY2 帧转换成 Windows GDI 可以绘制的HBITMAPMyVideo.avi则是录制功能的证据。改造时不要轻易绕开这两层——CvvImage的转换逻辑在老设备上比ColorConvert更稳定除非确认摄像头驱动支持 NV12 或 RGB24 直出。拿到工程后优先在 Debug 模式下跑通IDC_BUTTON_OPEN到预览显示的最小链路再考虑扩展抓拍、录像或者引入 OpenCV 做图像处理。每一步新增功能都做成独立按钮独立事件方便回溯到底是 DirectShow 链路的问题还是新模块的问题。本文还有配套的精品资源点击获取
返回列表