简介:这是一份面向DirectShow开发者的视频捕获示例工程,演示了如何基于Directshow Capture构建采集流程,并重点封装了RGB24与YV12、YVU9、YUY2等常见颜色空间互转函数,适合需要处理视频帧格式转换的C++开发者参考。工程包含20个文件,以6个头文件、4个CPP源文件为主,另有工程配置文件和cscc.lib静态库,函数接口清晰,可直接复用转换逻辑或嵌入其他视频项目。包体约55KB,轻量易读,便于快速定位核心代码。目前已有245人学习下载,对初学DirectShow或需要颜色空间转换工具的开发者来说,能够节省自行实现转换算法的时间,并帮助理解捕获与渲染流程中的格式处理环节。 很多人以为敲下“Dshow Capture”这组关键词,搜出来会是一款带注册码的截图工具。实际上完全不是一回事。Dshow 是 DirectShow 的缩写,Dshow Capture 指的是基于 Windows DirectShow 框架做视频采集,也就是把摄像头、采集卡这类视频源的画面变成可控的数据流,交给你的程序去预览、抓帧或者录制成文件。这套东西解决的是“Windows 上到底怎么把视频从硬件里拿过来”的最底层问题。
什么人需要搞懂它?做安防监控客户端、视频会议工具、机器视觉 Demo 的,或者单纯好奇摄像头数据是怎么进入到程序里的人,都绕不开。虽然微软后来主推 Media Foundation,但 DirectShow 生态仍然大量存在于老设备、厂家 SDK 和工业软件里,很多摄像头厂商的 SDK 底层走的还是 Dshow。把这套原理吃透,你再遇到黑屏、抢设备、格式不支持这些问题,就能自己定位,不用满世界找现成轮子。
这里也要先提醒一句:热搜里那些 FastStone Capture、OrCAD Capture、Capture One,全是不同领域的软件,碰巧都叫“capture”,千万别被带去别的赛道。下面我会把 Dshow Capture 从原理到实操完整拆一遍,最后再帮你区分那些容易“截胡”的邻居。
1. Dshow Capture到底是什么
1.1 先分清“Dshow”和那些同名Capture
视频采集这个词在国内技术圈被写得很乱,有些人叫视频捕获,有些人叫图像采集,英文里统一叫 capture。但 Dshow Capture 不是独立软件,不是某款能安装的工具,而是基于 DirectShow 框架做采集的一组代码逻辑。
DirectShow 是微软从 Windows 98 时代就开始推的一套多媒体框架,用来处理音频和视频的播放、采集、转码。它和现在开发者更熟悉的 Media Foundation 相比,确实显得老,但胜在兼容性好、文档多、第三方支持多。摄像头驱动尤其是 USB 摄像头驱动,基本都会提供 DirectShow 标准的接口,所以你买一个工业相机或者模拟采集卡,厂商给你的 SDK 里大概率还是用 DirectShow 做底层,再包一层自己的 API。
换句话说,你搜 Dshow Capture,说明你已经进入“要自己在代码里拿视频数据”这个阶段了。
1.2 DirectShow采集的核心框架
DirectShow 的核心思想是“把多媒体处理拆成一个个打通的积木”。这个积木叫 Filter,每个 Filter 上有多个 Pin(输入输出引脚),Filter 和 Filter 之间通过 Pin 连接,连成一条 Filter Graph(滤镜图)。
举个例子,你要做一个“摄像头画面显示在窗口”的采集功能,需要至少三个积木:
- Capture Filter:负责从硬件拿原始视频数据,这个通常由摄像头驱动提供。
- 中间处理 Filter:负责格式转换、抓帧,这部分可加可不加。
- Render Filter:负责把视频数据画到窗口或者文件里,比如 Video Renderer。
而所有 Filter 的操作都由一个叫 Filter Graph Manager 的总管来调度。跑采集流程前,你得先把这些积木按正确的方式插好、连好,然后发一个 Run 指令让水流(视频帧数据)流动起来。
这样设计的好处是模块化,坏处是概念多、接口繁。尤其是在没有图像界面辅助的情况下,全程用 COM 接口去拼装这一套图,新手很容易拼到一半就懵。这也是为什么我建议你先拿 GraphStudioNext 这一类工具把你电脑上的摄像头打开看一眼,直观感受一下 Filter 图长什么样,再回来写代码,会顺很多。
2. 开发前的环境准备和语言选型
2.1 环境搭建与SDK选择
Dshow 开发最常见的是 C++,需要装 Windows SDK 里的 DirectShow 头文件和库。注意,新版 Windows SDK 里 DirectShow 头文件被挪到了dshow.h、strmif.h这些文件里,你只需要在 Visual Studio 里新建项目,然后配置好“包含目录”和“库目录”就能编译。如果是老项目,可能还需要额外装 DirectX SDK,但现在一般不需要了。
如果觉得 C++ 的 COM 接口写起来太痛苦,C# 也是可以做的,但 .NET 本身没有官方 DirectShow 绑定,最常用的方案是引入一个开源库 DirectShowLib。这个库把 COM 接口都包成了 C# 接口,代码写起来会简洁很多。不过要注意,如果你要发布 C# 程序,需要把 DirectShowLib 一并发布,并且要注意目标平台是 x86 还是 x64,直接决定你能否正确枚举到摄像头。
我的建议是:如果只是做工具型软件,C# + DirectShowLib 上手最快;如果是做长期维护的工业项目,或者在已有 C++ 代码基础上加视频模块,老老实实写 C++ 更稳。两个我都试过,C++ 的排查路径更透明,出了问题更好定位。
2.2 设备枚举:先把摄像头“找出来”
无论是采集还是预览,第一件事永远是枚举设备,拿到设备列表。DirectShow 提供了一套系统设备枚举器,用起来大概是这个样子:
ICreateDevEnum* pSysDevEnum = NULL; HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSysDevEnum)); if (FAILED(hr)) return hr; IEnumMoniker* pEnum = NULL; hr = pSysDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnum, 0); if (hr == S_OK && pEnum != NULL) { IMoniker* pMoniker = NULL; while (pEnum->Next(1, &pMoniker, NULL) == S_OK) { IPropertyBag* pPropBag = NULL; hr = pMoniker->BindToStorage(0, 0, IID_PPV_ARGS(&pPropBag)); if (SUCCEEDED(hr)) { VARIANT varName; VariantInit(&varName); hr = pPropBag->Read(L"FriendlyName", &varName, 0); if (SUCCEEDED(hr)) { // varName.bstrVal 就是摄像头名称 } VariantClear(&varName); } } }这段代码实质上是先创建一个枚举摄像头分类的 COM 对象,然后让系统把所有符合条件的设备一个个列出来。设备的显示名称(FriendlyName)通常就是设备管理器里看到的名字,比如 “USB Camera”。如果你发现枚举出来空列表,先去设备管理器里确认摄像头被系统识别了没,别急着查代码。
2.3 构建设备Filter并加入Filter Graph
找到设备后,要把它实例化成一个 Filter 对象,并放入我们创建的 Filter Graph 中:
IGraphBuilder* pGraph = NULL; ICaptureGraphBuilder2* pBuilder = NULL; CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGraph)); CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pBuilder)); pBuilder->SetFiltergraph(pGraph); IBaseFilter* pCap = NULL; pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&pCap); pGraph->AddFilter(pCap, L"Capture Filter");这里有两个关键点:第一个是 Filter Graph Manager 必须先创建并且先 CreateCaptureGraphBuilder2,不然后面 RenderStream 会报错;第二个是 Capture Graph Builder 是一个额外辅助器,它封装了很多自动连接 Filter 的逻辑,手动拼图能拼到崩溃的场景,它往往一句就搞定了。
3. 核心实操:预览、抓帧、录像一条龙
3.1 从设备到窗口的预览
把摄像头的画面显示到窗口,DirectShow 提供了非常高效的一条连接链路:
pBuilder->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pCap, NULL, NULL);可别小看这一行,它背后做的事很多:自动选择 Capture Filter 上的 Preview Pin,自动创建必要的 Transform Filter(如果格式不支持),自动创建 Video Renderer,并进行媒体类型协商。RenderStream 的 PIN_CATEGORY_PREVIEW 参数表示这是预览通路,而非录制通路。
预览通路和录制通路在 DirectShow 里概念上是分开的,好处是同一个设备可以一边预览一边录像,坏处是如果你把 PREVIEW 和 CAPTURE 搞混了,容易出现画面正常但是没有数据进来、或者占着设备不松手的问题。大多数情况下,预览走 PREVIEW,保存走 CAPTURE,不要反。
3.2 设置分辨率和帧率
摄像头默认输出的格式往往不是你想要的,所以需要先查询设备支持哪些格式,再选择最合适的一组。查询支持格式用 IAMStreamConfig:
IAMStreamConfig* pConfig = NULL; pBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, IID_IAMStreamConfig, (void**)&pConfig); int count = 0, size = 0; pConfig->GetNumberOfCapabilities(&count, &size); for (int i = 0; i < count; i++) { AM_MEDIA_TYPE* pmt = NULL; BYTE* pSCC = new BYTE[size]; pConfig->GetStreamCaps(i, &pmt, pSCC); VIDEOINFOHEADER* vih = (VIDEOINFOHEADER*)pmt->pbFormat; // 这里可以看到宽高、编码格式、帧率 DeleteMediaType(pmt); delete[] pSCC; }如果你不想枚举,也可以直接修改当前格式再设置:
AM_MEDIA_TYPE* pmt = NULL; pConfig->GetFormat(&pmt); VIDEOINFOHEADER* vih = (VIDEOINFOHEADER*)pmt->pbFormat; vih->bmiHeader.biWidth = 1920; vih->bmiHeader.biHeight = 1080; pmt->subtype = MEDIASUBTYPE_MJPG; // 带宽不够时选JPEG压缩格式 pConfig->SetFormat(pmt);注意,分辨率不是想设多少就设多少。很多 USB 摄像头在 640x480 下支持 30fps YUY2,在 1920x1080 下可能只能跑 30fps MJPEG,而 RGB24 可能只支持低分辨率。如果你强行 SetFormat 到一个设备不支持的格式,接口会返回失败,画面会保持原有格式。这就是很多人“我怎么设 1080P 都没反应”的真实原因。
3.3 抓帧的两种实现姿势
抓取一帧图像,常用的是 ISampleGrabber。这个 Filter 可以插入到预览链路上,在每一帧流过时触发回调,让你把数据拷贝出来。
用 C# 且配合 DirectShowLib 时,写法大概是:
ISampleGrabber grabber = new SampleGrabber() as ISampleGrabber; grabber.SetMediaType(...); grabber.SetBufferSamples(true); grabber.SetOneShot(false); // 将 grabber 插入到 Graph 中C++ 里也类似,注意 SampleGrabber 的老接口依赖qedit.h,在较新的 Windows SDK 里这个头文件被删了,网上有不少历史项目还在贴老代码,编译不过时别慌,要么自己声明所需接口,要么直接找旧版 SDK 的 qedit.h 文件。这里踩坑的人特别多。
另一种抓帧姿势是使用 Capture Pin 上的回调机制,直接拿到原始 Bitmap 数据。这种方法比 SampleGrabber 灵活,但代码量明显更大,需要对 ALLOCATOR 生命周期有清晰认识。如果是 Demo 级别,先用 SampleGrabber 入门就行;如果是做产品,建议自己用 Capture Pin 加缓冲池,能更好地控制帧率。
3.4 录像落盘与编码选型
把视频保存成文件,最简单的路子是用 Avi Mux + File Writer 输出 AVI:
IBaseFilter* pAviMux = NULL; IBaseFilter* pFileWriter = NULL; CoCreateInstance(CLSID_AviMux, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pAviMux)); CoCreateInstance(CLSID_FileWriter, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileWriter)); IFileSinkFilter* pSink = NULL; pFileWriter->QueryInterface(IID_PPV_ARGS(&pSink)); pSink->SetFileName(L"capture.avi", NULL); pGraph->AddFilter(pAviMux, L"Avi Mux"); pGraph->AddFilter(pFileWriter, L"File Writer"); pBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, pAviMux, pFileWriter);这个方案录出来的 AVI 文件体积往往很大,因为默认可能是不压缩或轻压缩。如果你想录 H.264 的 MP4,DirectShow 原生路子并不方便,主流方案是两选一:要么直接用 FFmpeg 的 libavdevice,要么把 DirectShow 采集到的帧丢给 FFmpeg 编码再封装成 MP4。我自己更喜欢后者,因为 FFmpeg 的封装能力比 Avi Mux 强太多,而且跨平台时可复用同一套编码逻辑。
4. 老司机踩过的坑
4.1 黑屏与预览无画面
这是最常遇到的问题。第一个要排查的是 Filter Graph 是否真的连起来了。你可以用 GraphStudioNext 打开同一个设备,让软件自动构建一下,如果 GraphStudioNext 能出画面,说明是代码连接的问题;如果它也出不来,那就是设备、驱动或者格式的问题。
第二个常见原因是 RenderStream 之前没有正确设置窗口句柄。DirectShow 的视频渲染器默认需要一个窗口来承载画面,如果你没有指定窗口,它会自动弹出一个独立窗口来显示视频,有的场景里你以为“黑屏”,其实画面跑到那个弹出来的窗口里去了。需要调用 IVideoWindow 接口把 owner 设置为你的控件或窗口句柄。
第三个原因是设备被占用。摄像头被别的软件(比如 QQ、浏览器)占着时,DirectShow 采集通常会直接报错,此时你再去创建 Capture Filter 就会失败。先关闭占用摄像头的程序再试。
4.2 分辨率与帧率不达标
我遇到过不少开发在设置 4K 分辨率时,设备返回成功,但实际画面只有 1080P。这种问题的根源通常是设备固件支持多档输出,但 Dshow 驱动的 SetFormat 只是把内部状态改了,真正出流时还是会走默认的优先级。遇到这种情况,我通常会先遍历设备支持的 StreamCaps,把期望格式的输出能力一项项打出来,然后选一组设备“主动支持”的参数,而不是自己硬塞一个。
帧率低还有一个隐藏因素:USB 带宽。同一个 USB 控制器下挂很多设备,或者用了长的劣质延长线,都会导致摄像头传输不稳定。你把格式从 YUY2 改成 MJPEG,带宽开销能降一半以上,帧率立刻有改善。
4.3 COM 泄漏与内存使用持续增长
DirectShow 写起来容易,但内存泄漏非常隐蔽。每次枚举设备得到 IMoniker,用完要 Release;每次拿到 AM_MEDIA_TYPE,用完要 DeleteMediaType;每次 SampleGrabber 回调拿到 IMediaSample,用完要 Release。只要你漏了其中一个,长时间运行时内存就会逐渐攀升。
强烈建议在采集线程里加一个泄漏检测机制,Windows 自带的任务管理器每半小时截图对比一次,或者直接用 VLD 检测 C++ 的堆泄漏。我自己的习惯是,把抓帧和录制封装的类写成 RAII 风格,析构时统一释放所有 COM 指针,这样即使代码出错,释放动作也不会被漏掉。
4.4 设备热插拔与掉线恢复
摄像头在使用过程中被拔出,或者 USB 控制器休眠导致设备消失,是行业项目里必须处理的事。DirectShow 本身没有特别好的自动恢复机制,最实用的方案是:监听 WM_DEVICECHANGE 消息,收到设备移除事件后主动释放当前 Filter Graph,收到添加事件后重新枚举并自动重新打开设备。
这套流程做下来,用户体验上就是摄像头闪断后能自动恢复。不要想着“还在关键位置挂几秒就自己回来了”, DirectShow 的会话一旦断开,基本上就算废了,必须重建。
5. 那些容易被“截胡”的Capture们
我知道看完标题来搜的人,可能想找的不是 DirectShow,而是某一种“capture”工具。这里顺便帮大家把容易混淆的东西理清楚:
- FastStone Capture:Windows 上的截图和屏幕录制小工具,轻量、功能全,是付费软件,有试用期。如果搜“注册码”看到讨论,需要注意版权问题,在试用期结束后请购买正版授权,不建议去找破解。它和视频采集硬件没关系。
- Capture One:专业摄影后期软件,类似 Lightroom,主要是联机拍摄和 RAW 格式处理,跟摄像头采集完全两回事。
- OrCAD Capture:电子设计自动化工具里的原理图编辑器,用于电路原理图绘制,后面可以关联 PCB 设计。热搜里“capture 和 allegro 怎么关联”说的就是 OrCAD Capture 与 Allegro PCB 工具之间的联动,也是硬件设计方向。
- PaperStream Capture:富士通扫描仪的扫描管理软件,用于批量扫描文档并生成电子文件,属于办公扫描领域。
- “capture 变成 lite 版本怎么修复”:这类问题多出现在工具软件授权识别不正常的场景,一般从授权文件、登录状态、更新版本三方面入手排查,和 DirectShow 没有关系。
如果你的目的只是给某个软件找安装包或者解决报错,那方向可能就不对。真正在做摄像头数据采集、视频监控、机器视觉开发的,才是我上面花大力气讲的 Dshow Capture。判断标准很简单:你要的“capture”是不是要在代码里接摄像头?是,那就往下学 DirectShow;不是,那就去对应的领域找答案。
回到技术本身,这几年我接触过的摄像头项目里,真正难的不是“调用什么 API”,而是“搞不清为什么连接失败”。而这一切的根源,往往又回到没有理解 DirectShow 的 Filter Graph 模型上。你在驱动之上搭积木,积木怎么搭、哪里搭错,会直接决定后面每一步的稳定性。掌握了这套解决问题的思路,你用 DirectShow 还是 Media Foundation,用 C++ 还是 C#,都只是工具偏好问题。
最后分享一个实操中的小习惯:每次接新摄像头,我都会先把该设备支持的所有 StreamCaps 打出来存成日志,再写枚举代码去看它究竟暴露了几个 Device、有没有隐藏的 UVC 控制单元。这个习惯帮我避开了很多“黑屏五分钟”的加班现场。Dshow Capture 的资料虽老,但这套方法论到今天依然管用。
本文还有配套的精品资源,点击获取