ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Win10 下 DirectShow 开发环境搭建与 Filter 注册排错

Win10 下 DirectShow 开发环境搭建与 Filter 注册排错

简介:面向Windows 10平台多媒体开发者的DirectShow学习与开发包,适合需要处理视频音频流、构建播放器或捕获程序的初中级开发者,重点解决Win10下框架配置与COM接口调用等问题。压缩包为rar格式,体积约1.29MB,文件总数与类型明细上游暂未标注,内容围绕DirectShow开发所需的头文件、库文件及配套工具资源展开。已有919人学习,说明其在同类资料中具备一定参考度。资料覆盖DirectShow过滤器图与源、转换、渲染过滤器的基础知识,并延伸到Win10安装配置、CoCreateInstance调用、典型播放、捕获、流媒体、编辑场景,以及自定义过滤器、Filter Graph Manager管理、GraphEdit调试和HRESULT错误处理等要点。读者可据此梳理DirectShow开发脉络,对照关键接口与调试思路,减少在Win10环境搭建和排错上的摸索成本。

1. DirectShow 在 Windows 10 上的安装现场:为什么老工程一挪机器就编译不过

前几天把一份跑了很多年的采集 demo 挪到一台全新的 Windows 10 机器上,工程一个字符没改,编译直接红了一片:dxtrans.h找不到、strmbasd.lib链接不上,好不容易编译过了,运行时CoCreateInstance又甩回来一个0x80040154。DirectShow 在 Win10 上就是典型的「系统里有、开发环境里没有」——运行时组件跟着系统走,头文件、BaseClasses 源码、注册工具却要自己一件件补齐。这篇按我刚在这台机器上重装一遍的顺序写:装什么版本、目录怎么摆、库怎么链、filter 怎么注册、装坏了怎么退回去,适合第一次在 Win10 上搭 DirectShow 采集或播放工程的人照着走。

2. 装前清点:Windows 10 SDK 与 DirectShow BaseClasses 的版本搭配

2.1 三个组件的版本关系,先把这张表对一遍

DirectShow 本身不需要单独「安装」,它是 Windows 10 自带的一套 COM 组件,quartz.dll就躺在System32和SysWOW64里。真正要装的是开发期需要的那几样东西:SDK 提供的头文件和导入库、BaseClasses 提供的基类源码、以及一个能编 C++ 的编译器。这三者的版本关系如果不先对齐,后面每一步都会出问题。

组件常见选择在 DirectShow 工程里的作用是否必装
Visual Studio2019(v142)或 2022(v143)编译 .exe / .ax 工程,管运行库必装
Windows 10 SDK10.0.19041、10.0.22621 这类仍在维护的版本提供 dshow.h、strmif.h、uuids.h、strmiids.lib、quartz.lib必装
DirectShow BaseClasses资源包里的 BaseClasses 源码目录提供 CBaseFilter、CBasePin 等基类,编出 strmbase.lib写自定义 filter 时必装
GraphStudioNext / GraphEdit任意可用版本手动连 graph,验证 pin 能不能连上强烈建议
依赖查看工具任意查 .ax 的动态库依赖、查注册表可选

SDK 版本这块不用太纠结,DirectShow 的头文件好多年没大改过,10.0.19041 和 10.0.22621 编出来的结果没有区别。真正会出问题的是两件事:一是 BaseClasses 的源码版本和你手上的 SDK 是否匹配,太老的 BaseClasses 里会引用已经被移除的头文件;二是 MSVC 运行库,BaseClasses 用/MD编、你的工程用/MT链,链接期就会报 LNK2038 运行库不匹配,这个坑后面还会单独说。

还有一点容易被忽略:BaseClasses 不在 Windows SDK 的安装选项里。装 SDK 的时候勾不勾「Windows 桌面开发」都不会把streams.h和CBaseFilter送到你机器上,它属于旧版示例代码,得从资源包或者示例代码目录里单独拿。这也是很多人以为「SDK 装完就能写 filter」结果一头雾水的原因。

2.2 目录规划:头文件、库、源码分三处放

我见过太多工程把dshow.h、streams.h直接拷进项目目录,短时间能编译,换台机器或者升级 SDK 就全乱。常见的做法是分三层放置,工程里只写相对路径引用:

路径放什么谁维护
D:\DS\sdk_include\从 BaseClasses 里挑出来的头文件副本(streams.h、wxdebug.h 等)一次性拷贝,不随工程变
D:\DS\baseclasses\完整的 BaseClasses 源码,用于编 lib保持原样,不改动
D:\DS\build\lib\x64\、...\Win32\编出来的 strmbase.lib / strmbasd.lib编译产物,可删可重建
D:\DS\filters\自己写的 .ax 输出目录编译产物

把产物目录和源码目录分开,好处是清工程的时候直接删build,不用去源码里翻哪些是编出来的、哪些是原本就有的。资源包拿到手之后,我会先把 BaseClasses 整个目录复制到D:\DS\baseclasses,保持只读,任何修改都通过工程属性做,不动源码——这一条是血泪经验,改了源码下次换版本就再也对不上了。

2.3 Visual Studio 安装时到底要勾哪几项

打开 Visual Studio Installer,「修改」当前实例,在「单个组件」里确认这三项被勾上:MSVC v142 或 v143 生成工具(x86 与 x64 都要)、Windows 10 SDK(版本号记下来,后面配属性要用)、C++ 核心功能。三项缺任何一项,后面msbuild都会在第一步就失败。

装完之后验证一下,别急着开工程。开一个普通命令提示符,敲where msbuild,能返回路径就说明工具链在 PATH 里;再敲dir "C:\Program Files (x86)\Windows Kits\10\Include",能列出刚才那个 SDK 版本号目录,说明 SDK 头文件到位了。这两条命令我每次装完都会跑一遍,比起在 IDE 里报错之后再倒推,五分钟能省后面两小时。

3. 编译落地:从 BaseClasses 到 strmbase.lib 的四步走

3.1 第一步先编 BaseClasses,不要先编自己的工程

顺序反了会浪费很多时间。自己的工程一编译就报无法打开包括文件: streams.h,这时候你会以为是包含路径写错了,改半天路径,其实根源是strmbase.lib根本还不存在。正确顺序是先让 BaseClasses 编过,拿到 lib,再回头配主工程。

BaseClasses 目录里通常带一个baseclasses.vcxproj(也有叫baseclasses.sln的),直接命令行编最省事:

:: 进到 BaseClasses 源码目录再执行,路径按自己机器改 cd /d D:\DS\baseclasses :: x64 Release,产出 strmbase.lib msbuild baseclasses.vcxproj /p:Configuration=Release /p:Platform=x64 /m /v:m :: x64 Debug,产出 strmbasd.lib;32 位工程把 Platform 换成 Win32 msbuild baseclasses.vcxproj /p:Configuration=Debug /p:Platform=x64 /m /v:m

/p:Configuration和/p:Platform这两个参数决定了产物落到哪一层目录,Release 出strmbase.lib、Debug 出strmbasd.lib,名字带 d 的那个是 Debug 版,两者不能混用——Debug 工程链了 Release 的 lib,链接期不一定报错,运行时才在堆上出问题,这种属于最难查的一类。/m是并行编译,/v:m把输出降到简洁级别,报错信息依然完整,适合在命令行里看。

编完到x64\Release(或Win32\Release)下面确认一下 lib 大小,正常是几百 KB 级别。如果产物是 0 字节或者压根没生成,多半是 SDK 版本不匹配导致某些源文件被跳过,回去看编译输出里的第一条 warning。

3.2 第二步:工程属性里的三处路径怎么填

新建一个空的 C++ 控制台工程,然后按顺序改三处,不要用「编辑包含目录」那种一长串手填的方式,容易漏分号。

包含目录里加 BaseClasses 源码路径和刚才那个sdk_include;库目录里加 lib 产物路径。真正的关键在「附加依赖项」,这一项直接决定链接期会不会报一堆LNK2019。我一般直接改.vcxproj,比在对话框里点要清楚:

<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <Link> <!-- 顺序不重要,但 strmbase 必须和当前配置的 Debug/Release 对上 --> <AdditionalDependencies>strmbase.lib;quartz.lib;strmiids.lib;ole32.lib;oleaut32.lib;uuid.lib;winmm.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalLibraryDirectories>D:\DS\build\lib\x64\Release;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> </Link> </ItemDefinitionGroup>

逐个说清楚这几个库是干什么的,缺哪个会报什么错。strmbase.lib提供 CBaseFilter、CBasePin 这些基类实现,写自定义 filter 缺它必报基类未定义;quartz.lib提供 FilterGraph、CaptureGraphBuilder2 的 CLSID 和 IID,缺它会报CLSID_FilterGraph无法解析;strmiids.lib是各种接口 IID 的定义,IID_IBaseFilter这类符号从这里来;ole32.lib和oleaut32.lib是 COM 基础,CoInitializeEx、CoCreateInstance全靠它们;uuid.lib兜底一部分 GUID 定义;winmm.lib在用到多媒体定时器的时候需要。

一个常见的误用是把strmbasd.lib写死在工程里,然后切 Release 配置编译,链接器会提示找不到文件。正确做法是利用 VS 的配置宏,让 Debug 链带 d 的那个,或者干脆两套配置各写一份ItemDefinitionGroup。

3.3 第三步:最小骨架,先把 graph 建起来再谈业务

业务代码之前,先写一个只做三件事的程序:初始化 COM、创建 FilterGraph、创建 CaptureGraphBuilder2。这个骨架能跑通,说明头文件、库、运行库三样全对了,剩下的采集逻辑才有意义。

#include <windows.h> #include <dshow.h> #include <cstdio> #pragma comment(lib, "strmiids.lib") #pragma comment(lib, "quartz.lib") int main() { // 采集和播放类程序统一用 STA,跨线程回调里图状态才不会错乱 HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); if (hr == RPC_E_CHANGED_MODE) { // 当前线程已被别处用 MTA 初始化过,直接用,不要重复初始化 printf("COM 已以 MTA 初始化,沿用现有套间\n"); } IGraphBuilder* pGraph = nullptr; hr = CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)&pGraph); if (FAILED(hr)) { // 0x80040154 = 类未注册,多半是位数不匹配或系统组件被裁掉 printf("FilterGraph 创建失败: 0x%08X\n", hr); return -1; } ICaptureGraphBuilder2* pBuild = nullptr; hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, nullptr, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)&pBuild); if (FAILED(hr)) { printf("CaptureGraphBuilder2 创建失败: 0x%08X\n", hr); pGraph->Release(); return -1; } pBuild->SetFiltergraph(pGraph); // 把 builder 绑到这张图上,后面建链全靠它 printf("graph 创建成功\n"); pBuild->Release(); pGraph->Release(); CoUninitialize(); return 0; }

三个细节值得单独说。CoInitializeEx的第二个参数用COINIT_APARTMENTTHREADED(STA),因为 DirectShow 的回调和窗口消息绑得比较紧,MTA 下有些 filter 会出现状态不同步;如果返回RPC_E_CHANGED_MODE,说明这个线程之前被别的库初始化成 MTA 了,这时不要CoUninitialize,也不要再初始化一次。CoCreateInstance的返回值一定要打出来,0x80040154是类未注册、0x80070005是权限不足、0x8007007E是找不到模块,三个错误码指向完全不同的方向。最后,Release的顺序和创建顺序相反,CoUninitialize必须在所有 COM 对象释放之后调,这条违反了在调试版里会直接断言。

4. 运行环境配置:filter 注册、位数对齐与 qedit.h 兼容写法

4.1 位数对齐:32 位 filter 只能用 32 位 regsvr32 注册

这一条是 Win10 上最容易出现的问题,因为目录命名是反的。64 位系统里,C:\Windows\System32放的是 64 位程序,C:\Windows\SysWOW64放的是 32 位程序。你的 .ax 是 32 位,就必须用SysWOW64下面那个regsvr32.exe去注册;用错了,注册会「成功」,但程序里CoCreateInstance永远返回0x80040154。

:: 注册 32 位自定义 filter,必须以管理员身份运行,且用 SysWOW64 下的 regsvr32 C:\Windows\SysWOW64\regsvr32.exe /s "D:\DS\filters\Win32\Release\MyGrabber.ax" :: 注册 64 位 filter,用 System32 下的(注意不要写成 SysWOW64) C:\Windows\System32\regsvr32.exe /s "D:\DS\filters\x64\Release\MyGrabber.ax" :: 卸载用 /u,改代码重新编译前先卸一遍,避免旧版本残留在注册表里 C:\Windows\SysWOW64\regsvr32.exe /u "D:\DS\filters\Win32\Release\MyGrabber.ax"

/s是静默模式,不弹框,适合脚本化;调试阶段建议去掉它,让它把成功或失败的提示弹出来,因为DllRegisterServer内部失败时静默模式什么都不显示。注册完不要急着跑程序,先在注册表里确认InprocServer32指向的路径和你刚注册的文件是同一个,机器上装过多个版本的时候,这一步能省掉大量排查时间。

如果regsvr32报「模块已加载,但找不到入口点 DllRegisterServer」,说明这个 .ax 没导出注册函数,检查工程里是否定义了DllRegisterServer和对应的.def文件;如果报「找不到指定模块」,那是依赖的 DLL 缺失,用依赖查看工具查一遍,通常是某个运行库没装。

4.2 qedit.h 与 SampleGrabber:老代码在新 SDK 上怎么改

抓帧的工程十个有九个用到ISampleGrabber,而它的声明在qedit.h里。这个头文件在新版 SDK 上直接包含会报无法打开包括文件: dxtrans.h,因为qedit.h内部引了 DirectX Media 的一部分类型,而这块早就不随 SDK 发了。

我一般用屏蔽接口定义的方式绕过去,在包含qedit.h之前先把四个冲突的接口宏定义掉:

// 这四个宏告诉 qedit.h:相关接口已经声明过,不要再拉 dxtrans.h #define __IDxtCompositor_INTERFACE_DEFINED__ #define __IDxtAlphaSetter_INTERFACE_DEFINED__ #define __IDxtJpeg_INTERFACE_DEFINED__ #define __IDxtKey_INTERFACE_DEFINED__ #include <qedit.h>

原理很简单:qedit.h里那段引用是被#ifndef __IDxtCompositor_INTERFACE_DEFINED__这类条件包起来的,手动定义之后预处理阶段直接跳过,dxtrans.h就不会被拉进来。这四个宏必须写在#include <qedit.h>之前,写到后面无效,而且每个用到 SampleGrabber 的 .cpp 都要写,建议抽成一个ds_compat.h统一包含。

另一种做法是自己声明ISampleGrabber的虚表结构和 IID,完全不依赖qedit.h。这种做法在跨版本编译时更稳,代价是要手写几十行接口声明,容易写错方法顺序。除非你要在多个 SDK 版本之间来回切,否则不建议走这条路。

还有个链接期的细节:CLSID_SampleGrabber在quartz.lib里,IID_ISampleGrabber有时候得自己定义一份,报无法解析的外部符号 IID_ISampleGrabber就是这个原因。定义的时候 GUID 值要和系统里注册的完全一致,差一个字节都创建不出来。

4.3 设备枚举与 Win10 的两个权限开关

代码编过了、filter 注册了,接下来最常见的现象是枚举不到摄像头。先别怀疑代码,Win10 有个隐私开关:设置 → 隐私 → 相机,里面「允许应用访问你的相机」和「允许桌面应用访问你的相机」两个都要打开。第二个默认可能是关的,而 DirectShow 走的正是桌面应用这条通道。

枚举设备的写法比较固定,用系统设备枚举器按类别过滤:

// 枚举视频输入设备,注意 S_FALSE 是「这一类一个都没有」,不是失败 ICreateDevEnum* pDevEnum = nullptr; CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)&pDevEnum); IEnumMoniker* pEnum = nullptr; HRESULT hr = pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnum, 0); if (hr != S_OK) { // S_FALSE:系统里没有任何视频输入设备,先去看设备管理器 printf("没有找到视频输入设备\n"); } IMoniker* pMoniker = nullptr; while (pEnum && pEnum->Next(1, &pMoniker, nullptr) == S_OK) { IPropertyBag* pBag = nullptr; pMoniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, (void**)&pBag); VARIANT var; VariantInit(&var); // FriendlyName 就是设备管理器里显示的那个名字,用它判断选的是哪一个 pBag->Read(L"FriendlyName", &var, nullptr); if (var.vt == VT_BSTR) wprintf(L"设备: %s\n", var.bstrVal); VariantClear(&var); pBag->Release(); pMoniker->Release(); } if (pEnum) pEnum->Release(); pDevEnum->Release();

CreateClassEnumerator返回S_FALSE是很多人会误判的地方,它不是错误,只是这一类设备数量为零。这时候往下查的方向是驱动和设备管理器,而不是代码。另外注意Next返回S_OK才是拿到一个 moniker,循环条件写成== S_OK而不是SUCCEEDED(...),否则最后一个元素会被多处理一次。

5. 排查记录:DirectShow 在 Win10 上翻车的五个坑

5.1 编译期:头文件和库的坑

坑一:无法打开包括文件: dxtrans.h。现象是编译到某个用 SampleGrabber 的文件就中断,报错行指向qedit.h内部。原因是新版 Windows SDK 不再附带 DirectX Media 的那部分头文件,而qedit.h默认会去引。解决办法就是上一节那四个宏,写在#include <qedit.h>之前,全工程统一放一个兼容头文件里,不要每个文件各写一遍,容易漏。

坑二:LNK2038: 检测到“RuntimeLibrary”的不匹配项。现象是链接期报错,提示MD_DynamicRelease和MT_StaticRelease冲突。原因是 BaseClasses 编出来的 lib 用的是/MD,而你自己的工程在「C/C++ → 代码生成 → 运行库」里选成了/MT。解决是把主工程的运行库改成和 BaseClasses 一致的多线程 DLL(/MD),Debug 配置对应/MDd。不要反过来去改 BaseClasses 的工程设置,改了下次换版本又得重来。

坑三:LNK2019: 无法解析的外部符号 CLSID_FilterGraph。现象是编译通过、链接报一堆 GUID 未定义。原因是附加依赖项里少了quartz.lib或strmiids.lib。解决是按 3.2 节那张表把该加的库补齐,注意 Debug 和 Release 两份配置都要加,只改一份是最常见的疏忽。

5.2 运行期:注册与设备的坑

坑四:CoCreateInstance返回0x80040154(类未注册)。现象是自写 filter 在开发机上好好的,换一台机器就创建不出来。原因有三种:位数不匹配导致注册表里根本没有这条记录、regsvr32用错了目录(32 位文件用 System32 的注册器)、或者是注册成功了但注册表被 32/64 位重定向到了另一个分支。解决是先按 4.1 节的规则重注册一遍,再用reg query确认InprocServer32的路径确实指向你编译出来的那个 .ax。

坑五:graph 建起来了但采集黑屏、帧数为零。现象是 pin 都连上了,Run之后回调一次都不进。原因通常不是代码:Win10 的相机隐私开关没开、摄像头被另一个程序独占、或者用了MEDIASUBTYPE_RGB24而设备只出YUY2。解决顺序是先开隐私开关,再关掉所有可能占用摄像头的程序,最后用 GraphStudioNext 手动连一遍同样的 graph,看 pin 的媒体类型协商到了什么。手动连能出图、代码不能出图,问题一定在你的参数设置上,不在环境上。

这五个坑覆盖了从编译到运行的大半场景,剩下的基本是它们的变体。真正查不出来的时候,我会用依赖查看工具看 .ax 的动态库依赖,用 Process Monitor 看注册表读写落在哪个分支——这两样比反复改代码有效得多。

6. 装完怎么验证:三层验证法确认 graph 真的跑起来了

装完不代表能用,我习惯按三层往下验,任何一层不过就停在那一层查,不要跳。

第一层验「组件在不在」。命令行敲两条:

:: 系统自带的 DirectShow 核心组件,正常 Win10 两个目录都该有 dir C:\Windows\System32\quartz.dll dir C:\Windows\SysWOW64\quartz.dll :: 查自定义 filter 的注册信息,InprocServer32 必须指向实际文件 reg query "HKCR\CLSID\{你的Filter的CLSID}\InprocServer32" /ve

两条都有输出,且路径对得上,第一层算过。reg query报「系统找不到指定的注册表项」,说明 filter 没注册成功,回 4.1 节。

第二层验「graph 连不连得通」。打开 GraphStudioNext,Graph → Insert Filter把采集设备和渲染器拉进来,手动连 pin。连不上会直接标红并告诉你媒体类型协商失败在哪一步。这一层的价值在于把「代码问题」和「环境问题」彻底分开:手动连得上、代码连不上,就是参数写错了;手动也连不上,就是驱动或设备的问题。

第三层验「跑起来有没有数据」。写个最小循环,Run之后数回调次数,跑三秒看帧数:

验证项观察点期望结果
COM 初始化CoInitializeEx返回值S_OK或S_FALSE
设备枚举CreateClassEnumerator返回值S_OK,且至少一个设备
Graph 构建SetFiltergraph与RenderStream返回值S_OK
数据流三秒内回调次数大于零且大致等于帧率 × 3
退出清理释放后无断言Debug 版无弹窗

三秒回调次数为零但 graph 状态是 Running,回头看坑五;次数远低于帧率,检查是不是在回调里做了耗时操作阻塞了数据流线程。

从那以后我每次在新机器上装完 DirectShow,都会先用 GraphStudioNext 手动连一遍 graph 再动业务代码,这一步花五分钟,能挡掉后面大半天对着黑屏猜原因的工夫。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进