简介:这份资源面向在 Windows 10 平台从事多媒体开发的程序员与学习者,聚焦微软 DirectShow 框架的实战应用,帮助解决音视频播放、捕获、转码与流媒体处理等开发需求。压缩包为 rar 格式,整体约 1.29MB,内容涵盖 DirectShow 在 Win10 下所需的头文件、库文件及配套工具,可用于配置开发环境、调用 COM 接口实例化过滤器,并借助 GraphEdit 构建和调试过滤器图。资源围绕过滤器网络模型展开,涉及源过滤器、转换过滤器与渲染过滤器的协作机制,以及 Filter Graph Manager 对数据流的调度,适合构建自定义播放器、摄像头捕获、RTSP/MMS 流媒体接收和视频编辑等典型场景。目前已有 918 人学习下载,读者可据此掌握 Win10 下 DirectShow 的安装配置、API 调用、自定义过滤器开发与 COM 错误处理思路,是一份经过验证、上手门槛适中的多媒体开发参考资料。1. DirectShow_Win10(亲测可用):老采集框架在新系统上到底还能不能跑如果你手头有一批基于 DirectShow 的老采集程序,或者正打算在 Win10 上做摄像头、采集卡、虚拟摄像头的视频抓取,大概率会撞上同一个问题:代码在旧系统上跑得好好的,换到 Win10 就枚举不到设备、预览黑屏、回调不触发。DirectShow 这套框架年代久远,微软早就不主推了,但它在采集领域的稳定性和低延迟依然能打,所以「DirectShow_Win10(亲测可用)」这个方向一直有人搜。这篇笔记不讲空泛的历史,只讲我在 Win10 上把 DirectShow 采集链路跑通、跑稳的完整路径:从环境确认、设备枚举、Graph 搭建,到参数调优和几个必踩的坑。适合两类人:一是要维护老采集代码的工程师,二是想用最轻量的方式拿到摄像头裸帧、不想引入大型框架的开发者。DirectShow 在 Win10 上「能不能用」这个问题,答案是可以,但前提是你得接受它的一套脾气。它依赖的是系统自带的 quartz.dll 和一组 legacy 组件,Win10 默认还在,只是不再更新。真正让人翻车的往往不是框架本身,而是权限、驱动模型和线程模型这三块。下面按实际动手顺序展开,每一步都给可复现的命令和代码。2. 在 Win10 上把 DirectShow 采集环境搭起来:从组件确认到第一个枚举程序2.1 先确认系统里 DirectShow 组件是否完整Win10 家庭版和专业版都自带 DirectShow 核心库,但部分精简版镜像会裁掉。最直接的确认方式是看quartz.dll是否存在,以及用 GraphEdit 这类工具能否正常加载。我一般先跑一条命令确认文件在位:# 在管理员权限的 PowerShell 中执行,确认核心库存在 Test-Path "$env:SystemRoot\System32\quartz.dll" # 返回 True 表示核心库在位如果返回 False,说明系统组件被裁剪,需要从同版本完整镜像里补回,或者直接换一台机器。这一步看着简单,但我见过太多人卡在「代码没问题但就是跑不起来」,最后发现是系统镜像的问题。接着确认开发侧。用 Visual Studio 建一个 C++ 空项目,包含<dshow.h>,链接strmiids.lib、quartz.lib、ole32.lib。如果编译报找不到符号,八成是没链接strmiids.lib,这个库提供的是 CLSID 和 IID 定义,漏了它会在CoCreateInstance阶段直接失败。2.2 用最小代码枚举视频设备环境确认完,第一件事是枚举设备。DirectShow 用ICreateDevEnum配合CLSID_VideoInputDeviceCategory来列出所有视频输入设备。下面这段是我常用的最小枚举代码:#include <dshow.h> #include <iostream> #pragma comment(lib, "strmiids.lib") #pragma comment(lib, "ole32.lib") int main() { CoInitializeEx(nullptr, COINIT_MULTITHREADED); // 多线程套间,采集场景推荐 ICreateDevEnum* pDevEnum = nullptr; IEnumMoniker* pEnum = nullptr; // 创建系统设备枚举器 HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pDevEnum)); if (SUCCEEDED(hr)) { // 指定视频输入设备类别 hr = pDevEnum->CreateClassEnumerator( CLSID_VideoInputDeviceCategory, &pEnum, 0); if (hr == S_OK && pEnum) { IMoniker* pMoniker = nullptr; while (pEnum->Next(1, &pMoniker, nullptr) == S_OK) { IPropertyBag* pPropBag = nullptr; pMoniker->BindToStorage(nullptr, nullptr, IID_PPV_ARGS(&pPropBag)); VARIANT varName; VariantInit(&varName); // 读取设备友好名称 pPropBag->Read(L"FriendlyName", &varName, nullptr); std::wcout << L"设备: " << varName.bstrVal << std::endl; VariantClear(&varName); pPropBag->Release(); pMoniker->Release(); } pEnum->Release(); } pDevEnum->Release(); } CoUninitialize(); return 0; }这段代码的逻辑很直白:先初始化 COM,再创建设备枚举器,然后按视频输入类别拉出所有 moniker,逐个读取 FriendlyName。参数上唯一需要注意的是CoInitializeEx的套间模型。采集场景我强烈建议用COINIT_MULTITHREADED,因为 DirectShow 的回调默认跑在独立线程上,单线程套间会引入额外的消息泵负担,容易在长时间采集时出现卡顿。如果这段代码枚举不到任何设备,先别怀疑代码,按顺序查三件事:摄像头是否被其他程序独占、系统隐私设置里相机权限是否打开、设备管理器里驱动是否正常。Win10 的相机隐私开关是很多人忽略的点,关掉之后 DirectShow 枚举会直接返回空列表,而且不报错。2.3 搭建第一条采集 Graph 并拿到裸帧枚举通了,下一步是搭 Graph。DirectShow 的采集链路是Capture Filter -> SampleGrabber -> Null Renderer,SampleGrabber 负责在数据流经过时回调给你裸帧。下面是我常用的搭建骨架:// 假设已拿到目标设备的 IMoniker* pMoniker IBaseFilter* pCap = nullptr; pMoniker->BindToObject(nullptr, nullptr, IID_PPV_ARGS(&pCap)); IGraphBuilder* pGraph = nullptr; CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGraph)); pGraph->AddFilter(pCap, L"Capture"); // 创建 SampleGrabber ISampleGrabber* pGrabber = nullptr; CoCreateInstance(CLSID_SampleGrabber, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGrabber)); IBaseFilter* pGrabberF = nullptr; pGrabber->QueryInterface(IID_PPV_ARGS(&pGrabberF)); pGraph->AddFilter(pGrabberF, L"Grabber"); // 设置媒体类型,指定 RGB24 方便后续处理 AM_MEDIA_TYPE mt = {}; mt.majortype = MEDIATYPE_Video; mt.subtype = MEDIASUBTYPE_RGB24; pGrabber->SetMediaType(&mt); // 创建 Null Renderer 作为终点 IBaseFilter* pNull = nullptr; CoCreateInstance(CLSID_NullRenderer, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pNull)); pGraph->AddFilter(pNull, L"Null"); // 手动连接:Capture -> Grabber -> Null ICaptureGraphBuilder2* pBuilder = nullptr; CoCreateInstance(CLSID_CaptureGraphBuilder2, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pBuilder)); pBuilder->SetFiltergraph(pGraph); pBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, pGrabberF, pNull); // 设置回调,bufferSize 给 0 表示每帧都回调 pGrabber->SetBufferSamples(FALSE); pGrabber->SetOneShot(FALSE); pGrabber->SetCallback(&myCallback, 1); // 1 = 缓冲区模式这里有几个参数值得展开。SetMediaType指定 RGB24 是为了省去后续转换,代价是带宽比 YUY2 高,1080p 下每帧约 6MB,如果只是做检测可以改成 YUY2 再在回调里转。SetBufferSamples(FALSE)表示不复制样本到内部缓冲,回调里直接拿原始指针,延迟最低,但要求回调处理足够快,否则会丢帧。SetCallback的第二个参数 1 是缓冲区模式,0 是样本模式,缓冲区模式更省内存,样本模式更灵活,我一般用缓冲区模式。回调函数里拿到的是IMediaSample,用GetPointer取数据指针,GetActualDataLength取有效长度。注意回调线程不是主线程,任何 UI 操作都要 post 回主线程,这是新手最容易翻车的地方。3. 让采集稳定跑下去:分辨率、帧率与缓冲区的三个必调参数3.1 用 IAMStreamConfig 精确控制分辨率和帧率默认情况下 DirectShow 会协商一个它认为合适的格式,但往往不是你想要的。要精确控制,得拿到捕获 Filter 的IAMStreamConfig接口,遍历它支持的格式列表,选中最匹配的那个。下面这段是设置 1920x1080、30fps、YUY2 的典型写法:IAMStreamConfig* pConfig = nullptr; pBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, IID_PPV_ARGS(&pConfig)); int count = 0, size = 0; pConfig->GetNumberOfCapabilities(&count, &size); auto* pCaps = (VIDEO_STREAM_CONFIG_CAPS*)new BYTE[size]; for (int i = 0; i < count; ++i) { AM_MEDIA_TYPE* pmt = nullptr; if (SUCCEEDED(pConfig->GetStreamCaps(i, &pmt, (BYTE*)pCaps))) { if (pmt->formattype == FORMAT_VideoInfo) { auto* pvi = (VIDEOINFOHEADER*)pmt->pbFormat; // 匹配目标分辨率与帧率 if (pvi->bmiHeader.biWidth == 1920 && pvi->bmiHeader.biHeight == 1080 && pvi->AvgTimePerFrame == 333333) { // 30fps = 1/30 秒 pConfig->SetFormat(pmt); break; } } FreeMediaType(*pmt); } } delete[] pCaps;AvgTimePerFrame的单位是 100 纳秒,30fps 对应 333333,25fps 对应 400000。这个值必须和摄像头实际支持的对上,硬设一个不支持的值会导致SetFormat返回失败,然后 Graph 回退到默认格式,表现出来就是「我明明设了 1080p 结果出来是 640x480」。遍历时一定要用GetStreamCaps返回的真实能力列表,不要凭经验猜。3.2 缓冲区数量与回调耗时的平衡采集丢帧的根因通常不在采集端,而在回调处理太慢。DirectShow 内部有一个样本队列,队列满了新帧就会被丢弃。控制这个行为的关键是回调里的处理时间,以及SetBufferSamples的取值。我的经验是:回调里只做数据拷贝或入队,绝不做编解码、推理、写盘这类耗时操作。如果必须做,就再开一个线程,回调里只把指针和长度塞进无锁队列。下面是一个回调里只做入队的写法:class FrameCallback : public ISampleGrabberCB { public: STDMETHODIMP BufferCB(double, BYTE* pBuffer, long len) { // 只拷贝到环形缓冲,不做任何耗时处理 ringBuffer.Push(pBuffer, len); return S_OK; } STDMETHODIMP SampleCB(double, IMediaSample*) { return S_OK; } };环形缓冲的大小按「最大处理延迟 × 帧率」估算。比如后处理最慢要 200ms,30fps 下就是 6 帧,缓冲给 8 到 10 帧比较稳。给太小会丢帧,给太大会增加端到端延迟,这个取舍取决于你的场景是重实时还是重完整。3.3 用 Graph 事件处理采集异常采集跑久了难免遇到设备掉线、格式切换失败这类问题。DirectShow 通过IMediaEventEx把事件抛出来,不处理的话程序会静默卡死。我一般会起一个线程专门等事件:IMediaEventEx* pEvent = nullptr; pGraph->QueryInterface(IID_PPV_ARGS(&pEvent)); long evCode = 0; LONG_PTR p1 = 0, p2 = 0; while (pEvent->GetEvent(&evCode, &p1, &p2, 0) == S_OK) { if (evCode == EC_DEVICE_LOST) { // 设备掉线,触发重连逻辑 ReconnectDevice(); } pEvent->FreeEventParams(evCode, p1, p2); }EC_DEVICE_LOST是最需要处理的事件,USB 摄像头拔插、采集卡供电波动都会触发。重连逻辑里记得先Stop再RemoveFilter释放旧 Filter,否则第二次AddFilter会失败。这个坑我踩过不止一次,表现是重连后枚举正常但预览一直黑屏。4. DirectShow_Win10 避坑记录:五个让采集翻车的真实场景4.1 枚举返回空列表但设备管理器正常现象是代码跑起来一个设备都没有,设备管理器里摄像头却好好的。原因通常是 Win10 的相机隐私权限被关,或者应用跑在非交互式会话里。解决方式是进「设置 → 隐私 → 相机」确认允许桌面应用访问,同时确保程序不在服务会话中运行。DirectShow 枚举依赖交互式桌面会话,服务里跑必然拿不到设备。4.2 预览正常但 SampleGrabber 回调不触发现象是 Graph 能跑、能看到预览,但回调一次都不进。原因多半是SetCallback调用时机不对,必须在Run之前设置,而且SetBufferSamples和SetOneShot的组合要匹配。另一个常见原因是媒体类型没设对,Grabber 协商到了它不支持的格式就静默不回调。解决方式是先SetMediaType再连接,连接后调GetConnectedMediaType确认实际协商结果。4.3 长时间采集后内存持续上涨现象是跑几小时后内存占用越来越高。原因通常是回调里IMediaSample没释放,或者自己 new 的缓冲没 delete。DirectShow 的样本是引用计数的,回调里如果调了AddRef就必须配对Release。我一般用 RAII 包装,避免手动管理。另一个隐蔽原因是GetStreamCaps返回的AM_MEDIA_TYPE没调FreeMediaType,每次遍历都泄漏一点。4.4 多摄像头同时采集时其中一个卡顿现象是单路正常,两路一起跑就有一路掉帧。原因是 USB 带宽共享,两个 1080p 摄像头挂在同一个 USB 控制器上会互相抢带宽。解决方式是插到不同的 USB 控制器(看设备管理器里的控制器分组),或者降低其中一路的分辨率/帧率。软件层面没法绕过物理带宽限制,这是硬约束。4.5 切换分辨率后 Graph 状态异常现象是运行中调SetFormat切换分辨率,之后采集就乱了。原因是 DirectShow 的 Graph 在运行状态下改格式需要先Stop,改完再Run,直接改会导致内部状态不一致。正确做法是Stop -> SetFormat -> Run,并且切换后重新确认 SampleGrabber 的媒体类型,因为协商结果可能变了。5. 把 DirectShow 采集接进现代处理链路的一个实用技巧DirectShow 本身只负责把帧拿出来,真正让它有价值的是怎么把帧喂给后面的处理环节。我常用的做法是在回调里把裸帧写进一个共享内存环形缓冲,处理线程从缓冲里读,这样采集和处理彻底解耦,任何一边卡顿都不会拖垮另一边。共享内存用CreateFileMapping创建,结构上就是「头 + 数据区」,头里放写指针、读指针和帧计数,数据区按固定大小分槽。这个方案的关键参数是槽大小和槽数量。槽大小按最大分辨率算,1080p RGB24 一帧约 6MB,留点余量给 8MB。槽数量按「处理最慢耗时 × 帧率 × 2」估算,比如处理最慢 100ms、30fps,就是 6 个槽,给 8 个比较稳。读写指针用原子操作更新,避免加锁带来的抖动。验证这套链路是否健康,我一般看两个指标:一是采集回调的调用间隔是否稳定在帧率倒数附近,二是环形缓冲的积压槽数是否长期为 0。积压持续增长说明处理跟不上,要么降分辨率,要么优化处理。这两个指标用简单的计数器就能统计,不需要复杂工具。最后说个我自己的习惯:每次在新机器上部署 DirectShow 采集,第一件事不是跑业务代码,而是先跑那个最小枚举程序,确认设备列表正常,再跑一个只采集不处理的空转测试,看十分钟内有没有丢帧和内存增长。这两步过了,后面接什么处理都心里有底。DirectShow 在 Win10 上确实能用,只是它的脾气需要你用这套流程去摸清。希望帮到你。本文还有配套的精品资源,点击获取
简介:这份资源面向在 Windows 10 平台从事多媒体开发的程序员与学习者,聚焦微软 DirectShow 框架的实战应用,帮助解决音视频播放、捕获、转码与流媒体处理等开发需求。压缩包为 rar 格式,整体约 1.29MB,内容涵盖 DirectShow 在 Win10 下所需的头文件、库文件及配套工具,可用于配置开发环境、调用 COM 接口实例化过滤器,并借助 GraphEdit 构建和调试过滤器图。资源围绕过滤器网络模型展开,涉及源过滤器、转换过滤器与渲染过滤器的协作机制,以及 Filter Graph Manager 对数据流的调度,适合构建自定义播放器、摄像头捕获、RTSP/MMS 流媒体接收和视频编辑等典型场景。目前已有 918 人学习下载,读者可据此掌握 Win10 下 DirectShow 的安装配置、API 调用、自定义过滤器开发与 COM 错误处理思路,是一份经过验证、上手门槛适中的多媒体开发参考资料。1. DirectShow_Win10(亲测可用):老采集框架在新系统上到底还能不能跑如果你手头有一批基于 DirectShow 的老采集程序,或者正打算在 Win10 上做摄像头、采集卡、虚拟摄像头的视频抓取,大概率会撞上同一个问题:代码在旧系统上跑得好好的,换到 Win10 就枚举不到设备、预览黑屏、回调不触发。DirectShow 这套框架年代久远,微软早就不主推了,但它在采集领域的稳定性和低延迟依然能打,所以「DirectShow_Win10(亲测可用)」这个方向一直有人搜。这篇笔记不讲空泛的历史,只讲我在 Win10 上把 DirectShow 采集链路跑通、跑稳的完整路径:从环境确认、设备枚举、Graph 搭建,到参数调优和几个必踩的坑。适合两类人:一是要维护老采集代码的工程师,二是想用最轻量的方式拿到摄像头裸帧、不想引入大型框架的开发者。DirectShow 在 Win10 上「能不能用」这个问题,答案是可以,但前提是你得接受它的一套脾气。它依赖的是系统自带的 quartz.dll 和一组 legacy 组件,Win10 默认还在,只是不再更新。真正让人翻车的往往不是框架本身,而是权限、驱动模型和线程模型这三块。下面按实际动手顺序展开,每一步都给可复现的命令和代码。2. 在 Win10 上把 DirectShow 采集环境搭起来:从组件确认到第一个枚举程序2.1 先确认系统里 DirectShow 组件是否完整Win10 家庭版和专业版都自带 DirectShow 核心库,但部分精简版镜像会裁掉。最直接的确认方式是看quartz.dll是否存在,以及用 GraphEdit 这类工具能否正常加载。我一般先跑一条命令确认文件在位:# 在管理员权限的 PowerShell 中执行,确认核心库存在 Test-Path "$env:SystemRoot\System32\quartz.dll" # 返回 True 表示核心库在位如果返回 False,说明系统组件被裁剪,需要从同版本完整镜像里补回,或者直接换一台机器。这一步看着简单,但我见过太多人卡在「代码没问题但就是跑不起来」,最后发现是系统镜像的问题。接着确认开发侧。用 Visual Studio 建一个 C++ 空项目,包含<dshow.h>,链接strmiids.lib、quartz.lib、ole32.lib。如果编译报找不到符号,八成是没链接strmiids.lib,这个库提供的是 CLSID 和 IID 定义,漏了它会在CoCreateInstance阶段直接失败。2.2 用最小代码枚举视频设备环境确认完,第一件事是枚举设备。DirectShow 用ICreateDevEnum配合CLSID_VideoInputDeviceCategory来列出所有视频输入设备。下面这段是我常用的最小枚举代码:#include <dshow.h> #include <iostream> #pragma comment(lib, "strmiids.lib") #pragma comment(lib, "ole32.lib") int main() { CoInitializeEx(nullptr, COINIT_MULTITHREADED); // 多线程套间,采集场景推荐 ICreateDevEnum* pDevEnum = nullptr; IEnumMoniker* pEnum = nullptr; // 创建系统设备枚举器 HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pDevEnum)); if (SUCCEEDED(hr)) { // 指定视频输入设备类别 hr = pDevEnum->CreateClassEnumerator( CLSID_VideoInputDeviceCategory, &pEnum, 0); if (hr == S_OK && pEnum) { IMoniker* pMoniker = nullptr; while (pEnum->Next(1, &pMoniker, nullptr) == S_OK) { IPropertyBag* pPropBag = nullptr; pMoniker->BindToStorage(nullptr, nullptr, IID_PPV_ARGS(&pPropBag)); VARIANT varName; VariantInit(&varName); // 读取设备友好名称 pPropBag->Read(L"FriendlyName", &varName, nullptr); std::wcout << L"设备: " << varName.bstrVal << std::endl; VariantClear(&varName); pPropBag->Release(); pMoniker->Release(); } pEnum->Release(); } pDevEnum->Release(); } CoUninitialize(); return 0; }这段代码的逻辑很直白:先初始化 COM,再创建设备枚举器,然后按视频输入类别拉出所有 moniker,逐个读取 FriendlyName。参数上唯一需要注意的是CoInitializeEx的套间模型。采集场景我强烈建议用COINIT_MULTITHREADED,因为 DirectShow 的回调默认跑在独立线程上,单线程套间会引入额外的消息泵负担,容易在长时间采集时出现卡顿。如果这段代码枚举不到任何设备,先别怀疑代码,按顺序查三件事:摄像头是否被其他程序独占、系统隐私设置里相机权限是否打开、设备管理器里驱动是否正常。Win10 的相机隐私开关是很多人忽略的点,关掉之后 DirectShow 枚举会直接返回空列表,而且不报错。2.3 搭建第一条采集 Graph 并拿到裸帧枚举通了,下一步是搭 Graph。DirectShow 的采集链路是Capture Filter -> SampleGrabber -> Null Renderer,SampleGrabber 负责在数据流经过时回调给你裸帧。下面是我常用的搭建骨架:// 假设已拿到目标设备的 IMoniker* pMoniker IBaseFilter* pCap = nullptr; pMoniker->BindToObject(nullptr, nullptr, IID_PPV_ARGS(&pCap)); IGraphBuilder* pGraph = nullptr; CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGraph)); pGraph->AddFilter(pCap, L"Capture"); // 创建 SampleGrabber ISampleGrabber* pGrabber = nullptr; CoCreateInstance(CLSID_SampleGrabber, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGrabber)); IBaseFilter* pGrabberF = nullptr; pGrabber->QueryInterface(IID_PPV_ARGS(&pGrabberF)); pGraph->AddFilter(pGrabberF, L"Grabber"); // 设置媒体类型,指定 RGB24 方便后续处理 AM_MEDIA_TYPE mt = {}; mt.majortype = MEDIATYPE_Video; mt.subtype = MEDIASUBTYPE_RGB24; pGrabber->SetMediaType(&mt); // 创建 Null Renderer 作为终点 IBaseFilter* pNull = nullptr; CoCreateInstance(CLSID_NullRenderer, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pNull)); pGraph->AddFilter(pNull, L"Null"); // 手动连接:Capture -> Grabber -> Null ICaptureGraphBuilder2* pBuilder = nullptr; CoCreateInstance(CLSID_CaptureGraphBuilder2, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pBuilder)); pBuilder->SetFiltergraph(pGraph); pBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, pGrabberF, pNull); // 设置回调,bufferSize 给 0 表示每帧都回调 pGrabber->SetBufferSamples(FALSE); pGrabber->SetOneShot(FALSE); pGrabber->SetCallback(&myCallback, 1); // 1 = 缓冲区模式这里有几个参数值得展开。SetMediaType指定 RGB24 是为了省去后续转换,代价是带宽比 YUY2 高,1080p 下每帧约 6MB,如果只是做检测可以改成 YUY2 再在回调里转。SetBufferSamples(FALSE)表示不复制样本到内部缓冲,回调里直接拿原始指针,延迟最低,但要求回调处理足够快,否则会丢帧。SetCallback的第二个参数 1 是缓冲区模式,0 是样本模式,缓冲区模式更省内存,样本模式更灵活,我一般用缓冲区模式。回调函数里拿到的是IMediaSample,用GetPointer取数据指针,GetActualDataLength取有效长度。注意回调线程不是主线程,任何 UI 操作都要 post 回主线程,这是新手最容易翻车的地方。3. 让采集稳定跑下去:分辨率、帧率与缓冲区的三个必调参数3.1 用 IAMStreamConfig 精确控制分辨率和帧率默认情况下 DirectShow 会协商一个它认为合适的格式,但往往不是你想要的。要精确控制,得拿到捕获 Filter 的IAMStreamConfig接口,遍历它支持的格式列表,选中最匹配的那个。下面这段是设置 1920x1080、30fps、YUY2 的典型写法:IAMStreamConfig* pConfig = nullptr; pBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, IID_PPV_ARGS(&pConfig)); int count = 0, size = 0; pConfig->GetNumberOfCapabilities(&count, &size); auto* pCaps = (VIDEO_STREAM_CONFIG_CAPS*)new BYTE[size]; for (int i = 0; i < count; ++i) { AM_MEDIA_TYPE* pmt = nullptr; if (SUCCEEDED(pConfig->GetStreamCaps(i, &pmt, (BYTE*)pCaps))) { if (pmt->formattype == FORMAT_VideoInfo) { auto* pvi = (VIDEOINFOHEADER*)pmt->pbFormat; // 匹配目标分辨率与帧率 if (pvi->bmiHeader.biWidth == 1920 && pvi->bmiHeader.biHeight == 1080 && pvi->AvgTimePerFrame == 333333) { // 30fps = 1/30 秒 pConfig->SetFormat(pmt); break; } } FreeMediaType(*pmt); } } delete[] pCaps;AvgTimePerFrame的单位是 100 纳秒,30fps 对应 333333,25fps 对应 400000。这个值必须和摄像头实际支持的对上,硬设一个不支持的值会导致SetFormat返回失败,然后 Graph 回退到默认格式,表现出来就是「我明明设了 1080p 结果出来是 640x480」。遍历时一定要用GetStreamCaps返回的真实能力列表,不要凭经验猜。3.2 缓冲区数量与回调耗时的平衡采集丢帧的根因通常不在采集端,而在回调处理太慢。DirectShow 内部有一个样本队列,队列满了新帧就会被丢弃。控制这个行为的关键是回调里的处理时间,以及SetBufferSamples的取值。我的经验是:回调里只做数据拷贝或入队,绝不做编解码、推理、写盘这类耗时操作。如果必须做,就再开一个线程,回调里只把指针和长度塞进无锁队列。下面是一个回调里只做入队的写法:class FrameCallback : public ISampleGrabberCB { public: STDMETHODIMP BufferCB(double, BYTE* pBuffer, long len) { // 只拷贝到环形缓冲,不做任何耗时处理 ringBuffer.Push(pBuffer, len); return S_OK; } STDMETHODIMP SampleCB(double, IMediaSample*) { return S_OK; } };环形缓冲的大小按「最大处理延迟 × 帧率」估算。比如后处理最慢要 200ms,30fps 下就是 6 帧,缓冲给 8 到 10 帧比较稳。给太小会丢帧,给太大会增加端到端延迟,这个取舍取决于你的场景是重实时还是重完整。3.3 用 Graph 事件处理采集异常采集跑久了难免遇到设备掉线、格式切换失败这类问题。DirectShow 通过IMediaEventEx把事件抛出来,不处理的话程序会静默卡死。我一般会起一个线程专门等事件:IMediaEventEx* pEvent = nullptr; pGraph->QueryInterface(IID_PPV_ARGS(&pEvent)); long evCode = 0; LONG_PTR p1 = 0, p2 = 0; while (pEvent->GetEvent(&evCode, &p1, &p2, 0) == S_OK) { if (evCode == EC_DEVICE_LOST) { // 设备掉线,触发重连逻辑 ReconnectDevice(); } pEvent->FreeEventParams(evCode, p1, p2); }EC_DEVICE_LOST是最需要处理的事件,USB 摄像头拔插、采集卡供电波动都会触发。重连逻辑里记得先Stop再RemoveFilter释放旧 Filter,否则第二次AddFilter会失败。这个坑我踩过不止一次,表现是重连后枚举正常但预览一直黑屏。4. DirectShow_Win10 避坑记录:五个让采集翻车的真实场景4.1 枚举返回空列表但设备管理器正常现象是代码跑起来一个设备都没有,设备管理器里摄像头却好好的。原因通常是 Win10 的相机隐私权限被关,或者应用跑在非交互式会话里。解决方式是进「设置 → 隐私 → 相机」确认允许桌面应用访问,同时确保程序不在服务会话中运行。DirectShow 枚举依赖交互式桌面会话,服务里跑必然拿不到设备。4.2 预览正常但 SampleGrabber 回调不触发现象是 Graph 能跑、能看到预览,但回调一次都不进。原因多半是SetCallback调用时机不对,必须在Run之前设置,而且SetBufferSamples和SetOneShot的组合要匹配。另一个常见原因是媒体类型没设对,Grabber 协商到了它不支持的格式就静默不回调。解决方式是先SetMediaType再连接,连接后调GetConnectedMediaType确认实际协商结果。4.3 长时间采集后内存持续上涨现象是跑几小时后内存占用越来越高。原因通常是回调里IMediaSample没释放,或者自己 new 的缓冲没 delete。DirectShow 的样本是引用计数的,回调里如果调了AddRef就必须配对Release。我一般用 RAII 包装,避免手动管理。另一个隐蔽原因是GetStreamCaps返回的AM_MEDIA_TYPE没调FreeMediaType,每次遍历都泄漏一点。4.4 多摄像头同时采集时其中一个卡顿现象是单路正常,两路一起跑就有一路掉帧。原因是 USB 带宽共享,两个 1080p 摄像头挂在同一个 USB 控制器上会互相抢带宽。解决方式是插到不同的 USB 控制器(看设备管理器里的控制器分组),或者降低其中一路的分辨率/帧率。软件层面没法绕过物理带宽限制,这是硬约束。4.5 切换分辨率后 Graph 状态异常现象是运行中调SetFormat切换分辨率,之后采集就乱了。原因是 DirectShow 的 Graph 在运行状态下改格式需要先Stop,改完再Run,直接改会导致内部状态不一致。正确做法是Stop -> SetFormat -> Run,并且切换后重新确认 SampleGrabber 的媒体类型,因为协商结果可能变了。5. 把 DirectShow 采集接进现代处理链路的一个实用技巧DirectShow 本身只负责把帧拿出来,真正让它有价值的是怎么把帧喂给后面的处理环节。我常用的做法是在回调里把裸帧写进一个共享内存环形缓冲,处理线程从缓冲里读,这样采集和处理彻底解耦,任何一边卡顿都不会拖垮另一边。共享内存用CreateFileMapping创建,结构上就是「头 + 数据区」,头里放写指针、读指针和帧计数,数据区按固定大小分槽。这个方案的关键参数是槽大小和槽数量。槽大小按最大分辨率算,1080p RGB24 一帧约 6MB,留点余量给 8MB。槽数量按「处理最慢耗时 × 帧率 × 2」估算,比如处理最慢 100ms、30fps,就是 6 个槽,给 8 个比较稳。读写指针用原子操作更新,避免加锁带来的抖动。验证这套链路是否健康,我一般看两个指标:一是采集回调的调用间隔是否稳定在帧率倒数附近,二是环形缓冲的积压槽数是否长期为 0。积压持续增长说明处理跟不上,要么降分辨率,要么优化处理。这两个指标用简单的计数器就能统计,不需要复杂工具。最后说个我自己的习惯:每次在新机器上部署 DirectShow 采集,第一件事不是跑业务代码,而是先跑那个最小枚举程序,确认设备列表正常,再跑一个只采集不处理的空转测试,看十分钟内有没有丢帧和内存增长。这两步过了,后面接什么处理都心里有底。DirectShow 在 Win10 上确实能用,只是它的脾气需要你用这套流程去摸清。希望帮到你。本文还有配套的精品资源,点击获取