DirectShow实战:基于MFC实现摄像头采集与视频录制回放

发布时间:2026/9/16 4:09:03
DirectShow实战:基于MFC实现摄像头采集与视频录制回放 简介这是一份基于VC与DirectShow实现的摄像头视频采集及回放源码工程面向C/C开发者和多媒体编程学习者可用来理解Windows下的实时视频流处理、过滤图构建、视频解码渲染以及MFC桌面应用中的多线程与事件驱动编程。压缩包共22个文件主要包含4个h头文件、3个cpp实现文件以及dsp/dsw工程配置、rc资源文件、txt说明和ico图标等整体仅17KB适合直接阅读核心代码与工程结构。已有250人学习该资源。通过分析源码可以掌握视频采集与回放的过滤器图搭建流程、AVI/MP4等媒体格式的处理思路并学习Visual Studio调试技巧和错误处理机制是巩固VC多媒体开发能力的实用案例。1. 从摄像头取流到回放为什么还要自己写一遍很多人第一反应是 OpenCV 里VideoCapture(0)一行代码就能拿到摄像头画面何必去翻一份 VC 的 DirectShow 工程。但实际做设备端采集程序的人很快会发现OpenCV 的高层封装掩盖了太多关键细节设备枚举、分辨率切换、YUV 格式协商、帧率控制、预览窗口与录像文件同步。一旦硬件换成工业相机或 USB 摄像头或者需要把采集到的裸数据交给编码器VideoCapture的抽象层往往成了最大的限制。这份源程序的底层是 DirectShow通过 Filter Graph 把「采集 → 处理 → 预览」的链路显式地搭建出来读代码的人能清楚看到每一帧数据在哪个环节被处理、被丢弃、被写入文件。适合刚接触 DirectShow 但已经会用 C 的开发者也适合需要在 MFC 界面里嵌入视频预览窗口、处理摄像头热插拔和录像文件回放逻辑的人。2. DirectShow Filter Graph 与 MFC 工程的骨架2.1 Filter Graph 的组成方式与选型理由DirectShow 的核心思想是「过滤器图」。采集摄像头画面不是调用某个 SDK 函数而是把几个 COM 组件按顺序连接成一条流水线最左边是「源过滤器」负责从摄像头硬件拿数据中间是各种「转换过滤器」做格式转换、分辨率缩放最右边是「渲染过滤器」把视频画到窗口或者写入文件。这个工程里的VideoEXE目录通常就是那套可执行的测试程序而源码中与图构建相关的代码集中在CaptureGraphBuilder的调用上。之所以用 DirectShow 而不是 DirectShow.NET 或 Media Foundation是因为这个项目用 VC 6.0 时代的 MFC 写的DirectShow 的 COM 接口与 MFC 的消息循环天然契合而且 GraphEdit 工具可以直接把整个链路可视化调试时能直观看到哪个过滤器之间连接失败。// 典型的 Filter Graph 构建代码片段 IGraphBuilder* pGraph NULL; ICaptureGraphBuilder2* pBuilder NULL; IBaseFilter* pCapFilter NULL; IBaseFilter* pMuxFilter NULL; CoInitialize(NULL); CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)pGraph); CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)pBuilder); pBuilder-SetFiltergraph(pGraph); // 枚举摄像头设备并绑定源过滤器 ICreateDevEnum* pDevEnum NULL; CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)pDevEnum); // 后续通过 IEnumMoniker 遍历设备BindToObject 得到 pCapFilter这段代码的逻辑是先把 Filter Graph 管理器和 Capture Graph Builder 两个 COM 对象建出来前者管整条流水线的运行状态后者负责直接连接「视频源」到「写入文件」这一段。实际调试时最常踩的坑是把CLSID_FilterGraph写成了CLSID_FilterGraphNoClock后者没有时钟源录像的时间戳会出现跳变。2.2 MFC 界面与 DirectShow 窗口的桥接源程序里视频预览窗口不是靠 DirectShow 自己弹出而是用IVideoWindow接口把视频画面嵌入到 MFC 的控件区域。常见做法是拿到对话框上 Picture 控件的句柄然后调用SetOwner把渲染窗口的父窗口设为该控件。这里有个关键参数控件必须设置为SS_NOTIFY风格否则窗口尺寸变化时 DirectShow 不会收到重绘通知画面会花屏。// 将视频窗口绑定到 MFC 控件 IVideoWindow* pVW NULL; pGraph-QueryInterface(IID_IVideoWindow, (void**)pVW); pVW-put_Owner((OAHWND)GetDlgItem(IDC_PREVIEW)-GetSafeHwnd()); pVW-put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS); pVW-put_MessageDrain((OAHWND)GetSafeHwnd()); pVW-SetWindowPosition(0, 0, width, height);put_MessageDrain这一段容易被忽略它的作用是把 DirectShow 窗口收到的鼠标消息转交给 MFC 对话框处理否则你在视频画面上点击按钮、调整窗口位置都会失灵。尺寸参数在摄像头分辨率改变后要重新调用SetWindowPosition类似视频旋转这类操作也需要先停掉 Graph 再重建边运行边改窗口尺寸会导致渲染过滤器内部状态紊乱。2.3 Filter Graph 的运行状态管理源程序里对「开始采集」「停止采集」「单帧抓取」的控制是通过IMediaControl的Run、Stop、Pause三个方法实现的。很多人误以为Pause就是暂停录像实际上Pause的作用是让过滤器图进入就绪状态摄像头仍然在输出帧只是渲染被挂起。真正的录制暂停需要断开文件写入滤镜的数据流或者用IMediaSeeking做时间控制。方法状态变化常见用途Run()整个图开始流动开始预览或录制Pause()图就绪但数据不渲染准备首次画面显示避免黑屏Stop()所有过滤器复位数据指针重置停止采集、释放设备调用顺序也有讲究首次Run之前先Pause一次能让摄像头完成曝光初始化避免第一帧画面过亮。Stop之后要紧接着调用IBaseFilter的Stop方法否则部分 USB 摄像头在下次Run时会出现 30 秒以上的延迟。3. 视频采集的核心设备枚举与参数协商3.1 枚举摄像头设备并排除集成摄像头干扰这个工程最值得读的部分是设备枚举逻辑。ICreateDevEnum遍历系统设备时每条Moniker的 friendly name 对应一个摄像头但笔记本上通常有多个采集设备内置摄像头、USB 外接直接用第一个枚举结果容易踩坑。源程序里一般会按 USB Video Device 前缀做过滤或者列出所有设备让用户选择。从技术演进角度这个项目虽然是老代码但枚举接口在现代 Windows 10/11 上仍然可用只是部分摄像头驱动会把自身暴露成 UVC 兼容设备旧代码的匹配字符串需要同步调整。// 枚举摄像头设备并绑定源过滤器 ICreateDevEnum* pDevEnum NULL; CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)pDevEnum); IEnumMoniker* pEnum NULL; pDevEnum-CreateClassEnumerator(CLSID_VideoInputDeviceCategory, pEnum, 0); IMoniker* pMoniker NULL; while (pEnum-Next(1, pMoniker, NULL) S_OK) { IPropertyBag* pPropBag NULL; pMoniker-BindToStorage(0, 0, IID_IPropertyBag, (void**)pPropBag); VARIANT varName; VariantInit(varName); pPropBag-Read(LFriendlyName, varName, 0); // 在这里比对设备名选择目标摄像头 pMoniker-BindToObject(0, 0, IID_IBaseFilter, (void**)pCapFilter); VariantClear(varName); pPropBag-Release(); pMoniker-Release(); }CreateClassEnumerator的第三个参数如果是REGDB_E_CLASSNOTREG之外的错误码说明当前设备管理器里没有任何可用的视频输入设备。这类错误在虚拟机里特别常见源程序跑不起来往往不是代码问题而是宿主机的摄像头没有重定向到虚拟机。真实调试场景中我总是先在设备管理器里确认摄像头被识别为 UVC 设备再用AMCAP.EXE工具验证摄像头默认输出格式最后才跑工程里的测试程序。3.2 分辨率与像素格式的协商机制摄像头不是你说要 1920x1080 就给你 1920x1080。DirectShow 的IAMStreamConfig接口负责枚举并设置视频格式源程序里通常封装了一个SetVideoFormat函数内部遍历所有支持的媒体类型把分辨率、帧率、像素格式都拿出来对比。这里有个容易忽视的参数VIDEOINFOHEADER里的AvgTimePerFrame是单位 100ns 的帧间隔设定 30fps 就是这个字段设为 333333。如果直接改BitmapInfoHeader.biWidth而不动AvgTimePerFrame画面尺寸会变但帧率保持默认预览时会看到明显的卡顿感。// 设置分辨率与帧率核心是匹配媒体类型 IAMStreamConfig* pStreamConfig NULL; pBuilder-FindInterface(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, pCapFilter, IID_IAMStreamConfig, (void**)pStreamConfig); int iCount 0, iSize 0; pStreamConfig-GetNumberOfCapabilities(iCount, iSize); for (int i 0; i iCount; i) { BYTE* pParams new BYTE[iSize]; AM_MEDIA_TYPE* pMediaType NULL; pStreamConfig-GetStreamCaps(i, pMediaType, pParams); if (pMediaType-formattype FORMAT_VideoInfo) { VIDEOINFOHEADER* pVih (VIDEOINFOHEADER*)pMediaType-pbFormat; if (pVih-bmiHeader.biWidth 1280 pVih-bmiHeader.biHeight 720) { // 找到目标分辨率应用该格式 pStreamConfig-SetFormat(pMediaType); break; } } // 释放 AM_MEDIA_TYPE 内部数据 // 注意 delete pMediaType-pbFormat 与 delete pMediaType }这段逻辑里GetStreamCaps返回的AM_MEDIA_TYPE是从驱动层拷贝出来的数据用完后必须手动释放pbFormat和pMediaType本身否则每次切换分辨率就会泄漏十几个字节。多频切换分辨率测试几次之后进程内存涨得不多但句柄数量会明显上升最终导致CoCreateInstance失败。这就是典型的内存泄漏问题。3.3 预览与录制的并行链路工程里的视频回放不是直接从文件读而是把「预览」和「采集」做成两条分支摄像头源过滤器后面接一个Smart Tee过滤器这个过滤器把一路数据拆成两路一路走预览窗口另一路走文件写入。Smart Tee在预览和录制并行时是必需的但它的下游必须有一个渲染过滤器消耗数据否则整条链路会停住。// 用 Smart Tee 分流捕获一路进文件一路进预览 IBaseFilter* pSmartTee NULL; CoCreateInstance(CLSID_SmartTee, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)pSmartTee); pGraph-AddFilter(pSmartTee, LSmartTee); // 用 Capture Graph Builder 连接分流后的两个 PIN pBuilder-RenderStream(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, pCapFilter, NULL, pSmartTee); pBuilder-RenderStream(PIN_CATEGORY_PREVIEW, MEDIATYPE_Video, pSmartTee, NULL, NULL);RenderStream的第二个分支是预览路径它的下游参数传NULL让 DirectShow 自动选择合适的 Video Renderer。如果系统里有多个渲染器VMR7、VMR9、EVR老工程默认走 VMR7在 Win10 上容易出现黑屏。遇到这种情况可以手动给预览链路加一个CLSID_VideoMixingRenderer9的过滤器而不是改全局默认渲染器。4. 视频回放与文件格式处理4.1 用 DirectShow 回放视频文件回放功能在工程里的实现路径和采集完全不同。采集走的是捕获图回放走的是播放图。IGraphBuilder::RenderFile会根据文件扩展名找到匹配的源过滤器、解码器和渲染器。这个工程能放 AVI、MP4但 MP4 能否播放取决于系统里是否装了对应解码器DirectShow 本身不带 MP4 的 H.264 解码器Win10 上通常依赖系统自带的 Media Foundation 组件在 Win7 上就需要第三方解码器。// 回放核心逻辑RenderFile 后控制运行 IGraphBuilder* pPlayGraph NULL; IMediaControl* pPlayControl NULL; IMediaSeeking* pSeek NULL; CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)pPlayGraph); pPlayGraph-QueryInterface(IID_IMediaControl, (void**)pPlayControl); pPlayGraph-QueryInterface(IID_IMediaSeeking, (void**)pSeek); WCHAR wszFile[MAX_PATH] {0}; MultiByteToWideChar(CP_ACP, 0, strFilePath.c_str(), -1, wszFile, MAX_PATH); pPlayGraph-RenderFile(wszFile, NULL); pPlayControl-Run(); // 进度跳转单位是 100ns LONGLONG llPos 30LL * 10000000LL; pSeek-SetPositions(llPos, AM_SEEKING_AbsolutePositioning, NULL, AM_SEEKING_NoPositioning);RenderFile失败时常见错误是VFW_E_CANNOT_RENDER_FILE这个错误提示的是「整个图里没有哪个过滤器组合能处理这个文件」要么缺解码器要么文件损坏。用从工程源码里找到的graphedt.exe打开同一个文件能看到具体断在哪一环排查效率比盲目装解码器快得多。4.2 AVI Mux 与文件写入参数采集录像写入文件时工程里用的是AVI Mux过滤器它把预览链路送来的数据封装成 AVI 容器。AVI 容器的特点是索引区在文件末尾程序意外崩溃时录制文件会打不开因为索引没有写入。源程序里如果有「停止录制后自动修复」的逻辑大概率是通过IMediaDet重新扫描帧数据来恢复索引但更稳妥的做法是在Stop之前让AVI Mux正常收到EC_VIDEO_SIZE_CHANGED事件结束通知然后才断开图连接。// 写入 AVI 文件RenderStream 到文件写入链路 IBaseFilter* pMux NULL; IFileSinkFilter* pSink NULL; CoCreateInstance(CLSID_AviDest, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)pMux); pGraph-AddFilter(pMux, LAVI Mux); pBuilder-SetOutputFileName(MEDIASUBTYPE_Avi, Lcapture.avi, pSink, NULL); pBuilder-RenderStream(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, pCapFilter, NULL, pMux);SetOutputFileName会自动把 AVI Mux 和 File Writer 过滤器串接起来所以代码里不需要手动加 File Writer。文件路径需要绝对路径相对路径会让File Writer基于当前工作目录解析在调试器里跑和直接双击 exe 跑的工作目录不同录制文件会落得到处都是。源程序里往往把路径写死在GetCurrentDirectory拼接的位置这是老工程的一种坏习惯拿到后建议改成GetModuleFileName获取 exe 所在目录再拼接录制文件名。4.3 首次实现录像回放时遇到的若干典型场景在回放与采集的切换过程中最常见的问题是画面显示不连贯。这通常不是因为过滤器处理速度慢而是视频帧的时间戳不正确。摄像头直接采集的帧没有时间戳AVI Mux会按照「帧到达顺序」写入文件如果摄像头输出的是 30fps 但AvgTimePerFrame写的是 15fps 对应的值播放器就会以 15fps 的速度播放视觉上看起来就是慢动作。排查这个问题时可以直接用GSpot之类工具查看录像文件的实际帧率再与代码里VIDEOINFOHEADER的AvgTimePerFrame对比。// 检查 AVI 文件实际帧率的小工具写法 // 用 IMediaDet 读取文件视频流的帧率 IMediaDet* pDet NULL; CoCreateInstance(CLSID_MediaDet, NULL, CLSCTX_INPROC_SERVER, IID_IMediaDet, (void**)pDet); pDet-put_Filename(Lcapture.avi); AM_MEDIA_TYPE mt; pDet-get_StreamMediaType(mt); VIDEOINFOHEADER* pVih (VIDEOINFOHEADER*)mt.pbFormat; double dFps 10000000.0 / pVih-AvgTimePerFrame; // 输出 dFps 与实际采集帧率对比偏差大说明源过滤器时间戳有问题这个检查手段在源程序里没有直接封装但代码分析阶段用它能快速定位「录像文件时长与真实时间不符」的问题。还有一种情况是摄像头输出隔行扫描信号DirectShow 采集到的帧是交错排列的写入 AVI 后在普通播放器上看会有横纹这是格式问题而非程序 bug需要添加Video Mixing Renderer的 deinterlace 设置去处理。5. GraphEdit 验证与录制参数打磨5.1 用 GraphEdit 导出当前图结构快速排查链路问题拿到这套源码后最先应该做的是把编译出的程序跑起来然后在IMediaControl::Run之前加一行阻塞代码或者直接断点停在Run调用处用 GraphEdit 的「连接到远程图」功能查看当前已构建的过滤器图。Win10 系统自带 GraphEdit 已经被移除了但可以从旧 SDK 里提取graphedt.exe它仍然能连接 32 位进程里的 DirectShow 图。访问方式是用管理员权限启动 GraphEdit然后在「File - Connect to Remote Graph」里选择目标进程选中之后能看到所有过滤器的连接状态。这是这个工程最重要的调试手段因为RenderStream的返回值只能告诉你某条链路失败而图结构能告诉你失败的具体原因PIN 类型不匹配、缺少中间过滤器还是多个过滤器争抢同一个媒体类型。// 在 Run 之前故意延时留出时间用 GraphEdit 连接 pControl-Pause(); Sleep(8000); // 趁这段时间把 GraphEdit 挂到进程上 pControl-Run();Pause之后图已经构建完成但还没有开始流动此时连接 GraphEdit 能看到完整的图结构并且可以手动在 GraphEdit 里断开某个连接、插入一个新的转换过滤器实时测试是否能让链路走通改完后再回到代码里做相应调整。这种方式相比在代码里打日志看HRESULT要直观得多。5.2 录像文件时长不一致与帧率跳变的参数修正源程序在采集场景里默认使用AvgTimePerFrame作为帧率基准但在实际摄像头设备上帧与帧之间的间隔并不是恒定的。USB 摄像头受到带宽和系统调度影响单帧传输时间可能在 30ms 到 50ms 之间波动如果录像段特别长AVI 文件的播放时长与真实流逝时间之间的偏差就会累积。对于这个工程实现的录制功能建议把AVI Mux的帧率字段显式固定为摄像头标称帧率并开启IAMStreamConfig的帧率复制标志。同时在源程序的对话框资源中录像按钮的状态机切换也要处理得干净开始录制后立刻禁用分辨率下拉框因为采样格式的中途变更会导致AVI Mux内部缓冲区的边界错乱最终生成的文件会出现花屏或段错误。检查这种问题的办法是录制一段固定时长比如 10 秒的视频然后对比播放器的进度条时间与秒表是否一致偏差超过 0.5 秒就说明帧率基准需要手动修正。5.3 当前硬件环境下对老代码的三处修饰如果这台机器的摄像头只支持 YUY2 裸格式录制文件体积会很大压缩选项需要在AVI Mux之前手动挂一个编码过滤器。常见做法是用CLSID_MJPGEncMotion JPEG 编码器处理静态画面为主的场景文件体积能缩小到原来的五分之一画质损失肉眼几乎不可见。工程源码里一般不会预置编码器过滤器的构建代码需要在RenderStream之前手动AddFilter并指定编码器的输出媒体类型。另外在回调函数里处理EC_VIDEO_SIZE_CHANGED事件时应在Run状态下等待事件到达后再更新窗口尺寸别在消息循环里同步刷新。最后如果你要用这个源程序实现「录像的同时做运动检测」不要尝试在渲染线程里直接访问帧数据要把帧拷贝这一动作封装进一个独立线程在 DirectShow 的SampleCB回调里只做队列写入由后台线程完成检测和标记。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询