首页 / 资讯中心 / 文章详情

DirectShow虚拟摄像头实现详解:过滤器模型与帧数据流

DirectShow虚拟摄像头实现详解:过滤器模型与帧数据流 ★ FEATURED ARTICLE
简介一份面向DirectShow开发者和视频处理学习者的VCAM虚拟摄像头完整实现工程。方案以DirectShow API构建软件摄像头在无物理设备时输出连续视频流适用于测试、演示及多软件集成。工程系统展示了源过滤器、转换过滤器、渲染过滤器的设计要点覆盖过滤图构建、Filter Graph Manager管理、Sample Grabber抓帧、VMR渲染、虚拟设备注册等关键步骤并针对Windows 10与Visual Studio 2015环境进行兼容性适配包含多线程数据生成与传递、错误处理及性能优化思路。压缩包共154个文件以h/cpp源码为主辅以ax过滤器文件、vcxproj/sln工程文件、obj编译中间文件、pdb调试文件等便于直接编译、断点调试与二次开发整体大小约71.33MB。目前已有2450人学习下载适合具备Windows多媒体编程基础、希望深入理解或快速搭建虚拟摄像头的工程师参考。1. DirectShow虚拟摄像头让软件把视频文件当成摄像头的经典方案经常有朋友问我视频会议只能调用摄像头能不能把一段做好的短视频当成摄像头信号推出去答案是能而且Windows早就留好了入口——DirectShow虚拟摄像头VCAM就是干这个的。它本质上是一个系统层注册的假采集设备应用层打开摄像头时实际拿到的是你指定的图片、视频或动态生成画面。这套方案对直播推流、视频会议测试、自动化UI录制、教学演示都适用。如果你是C开发者想搞懂Windows多媒体框架下设备从注册到出流的完整链路VCAM是个不可多得的参考样例代码量不大但涉及过滤器、Pin、媒体协商、帧数据搬运这些核心概念值得拆开来逐个看。2. 架构拆解过滤器模型、Pin连接与帧数据流2.1 过滤器与PinDirectShow的乐高积木模型DirectShow把多媒体处理拆成过滤器Filter的链式组合源过滤器产生数据变换过滤器处理数据渲染过滤器输出数据。过滤器之间通过Pin连接Pin负责协商媒体类型和传输数据。VCAM虚拟摄像头是一个源过滤器它的输出Pin对外提供视频帧下游可以是任何能消费视频的过滤器比如视频渲染器、编码器或网络推流模块。这个模型对应用层是透明的。QQ、微信、Zoom这类软件枚举视频采集设备时系统会把注册在视频输入设备类别的CLSID全部列出来VCAM注册进去之后就和真实摄像头并列呈现选中它等于选中VCAM的数据源。要理解VCAM怎么骗过系统关键是看三个层面的配合注册表把CLSID映射到DLL路径DLL导出Com接口让系统可以创建过滤器实例过滤器内部实现采集Pin和帧生成逻辑。三者缺一不可——DLL没导对函数regsvr32直接报错Pin没实现媒体协商应用打开设备黑屏帧数据生成太快或太慢画面比例或帧率就出问题。2.2 VCAM的模块划分采集Filter、属性页与配置中心VCAM的代码组织结构并不复杂核心模块可以拆成四块。采集过滤器是设备的主体直接继承DirectShow的CSource基类内部包含输出Pin的实例负责响应Graph的拉帧请求。属性页是给用户看的配置界面实现IPropertyPage接口常规属性页用自绘窗口或控件实现VCAM这里用了一个所见即所得的设置页能选图片文件、设分辨率、调帧率。第三块是帧生成器它不是独立线程而是输出Pin的线程回调函数——DirectShow的CSourceStream会启动一个工作线程持续调用FillBuffer方法要帧数据VCAM在这个回调里把图片像素拷贝到IMediaSample缓冲区。第四块是配置持久化用注册表存上次选择的文件路径和参数下次打开自动恢复。2.3 目录结构与关键文件定位一套典型的VCAM项目源码目录结构大概是这样的VCAM/ ├── VCAM.sln ├── VCAM/ │ ├── VCAM.cpp # DLL入口、类工厂、注册表注册逻辑 │ ├── VCAM.h # 过滤器类声明 │ ├── VCAMStream.cpp # 输出Pin实现核心的帧生成逻辑 │ ├── VCAMStream.h │ ├── VCAMProp.cpp # 属性页实现 │ ├── VCAMProp.h │ ├── resource.h │ └── VCAM.def # DLL导出定义文件 └── README.mdVCAM.cpp里最重要的是DllRegisterServer和DllUnregisterServer两个函数regsvr32调用就是进入这里。VCAMStream.cpp是重点阅读对象FillBuffer和GetMediaType都在这。VCAMProp.cpp属于改造友好区域想加新的配置项改这一个文件加对应的过滤器接口方法就行。3. 核心实现采集Filter从初始化到持续输出帧的全过程3.1 继承CSource最小可运行的DirectShow采集过滤器骨架DirectShow提供了CSource和CSourceStream两个辅助类把大量繁琐的COM样板代码封装好了。VCAM的过滤器类继承CSource在构造函数里new出一个输出Pin实例。class CVCam : public CSource { public: CVCam(); virtual ~CVCam() {} static CUnknown* WINAPI CreateInstance(IUnknown* pUnk, HRESULT* phr); private: CVCamStream* m_pStream; // 输出Pin }; CVCam::CVCam() : CSource(NULL, CLSID_VCam, m_hr) { m_pStream new CVCamStream(m_hr, this, LVCam Out); if (FAILED(m_hr)) { delete m_pStream; m_pStream NULL; } }CreateInstance是给类工厂调用的入口DirectShow用IClassFactory机制创建实例时走这里。CSource的构造函数接受CLSID作为参数这个CLSID就是注册表里那个GUID。m_hr是COM标准做法存储构造过程中的错误码失败时上层能拿到具体原因。3.2 FillBuffer帧数据的生成与搬运应用层和VCAM之间是拉模型——DirectShow的Graph中下游过滤器通过IMemAllocator拿到空缓冲区后通知源过滤器填充数据。CSourceStream内部开了一个工作线程循环调用FillBuffer来填充每一帧。HRESULT CVCamStream::FillBuffer(IMediaSample *pSample) { BYTE* pData NULL; pSample-GetPointer(pData); long cbData pSample-GetSize(); // 从bmp文件加载的DIB数据转换成RGB24格式填充 if (m_pBmpData cbData m_nFrameSize) { memcpy(pData, m_pBmpData, m_nFrameSize); } // 设置时间戳单位是100纳秒 CRefTime tStart m_rtStart; CRefTime tStop m_rtStart m_dwFramePeriod; pSample-SetTime(tStart, tStop); m_rtStart tStop; return S_OK; }m_pBmpData是预先加载到内存的位图像素数据这样每次FillBuffer只做一次memcpy速度很快。时间戳决定了下游渲染器播放的节奏如果时间戳间隔不对视频会出现快进或卡顿。注意这里没有Sleep因为DirectShow是根据时间戳调度播放的帧线程跑多快取决于下游消费速度。3.3 GetMediaType与DecideBufferSize媒体协商的关键环节Media Type协商是DirectShow最核心也最容易出错的环节。上层应用打开设备时会问这个Pin支持哪些格式也就是分辨率、像素格式、帧率。VCAM在GetMediaType中返回一个VIDEOINFOHEADER结构来描述支持的格式。HRESULT CVCamStream::GetMediaType(int iPosition, CMediaType *pmt) { if (iPosition 0) return E_INVALIDARG; if (iPosition 1) return VFW_S_NO_MORE_ITEMS; VIDEOINFOHEADER* pvi (VIDEOINFOHEADER*)pmt-AllocFormatBuffer(sizeof(VIDEOINFOHEADER)); ZeroMemory(pvi, sizeof(VIDEOINFOHEADER)); pvi-bmiHeader.biSize sizeof(BITMAPINFOHEADER); pvi-bmiHeader.biWidth m_nWidth; pvi-bmiHeader.biHeight m_nHeight; pvi-bmiHeader.biPlanes 1; pvi-bmiHeader.biBitCount 24; pvi-bmiHeader.biCompression BI_RGB; pvi-bmiHeader.biSizeImage m_nFrameSize; pmt-SetType(MEDIATYPE_Video); pmt-SetSubtype(MEDIASUBTYPE_RGB24); pmt-SetFormatType(FORMAT_VideoInfo); pmt-SetTemporalCompression(FALSE); return S_OK; }这段代码说明VCAM默认只输出RGB24格式。很多软件默认请求YUY2或者NV12格式这时候VCAM就无能为力了。双格式支持需要在SetMediaType和CheckMediaType里做分支处理这就是二次开发最常动的地方。再加上DecideBufferSize中要保证缓冲区够大HRESULT CVCamStream::DecideBufferSize(IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pProperties) { ALLOCATOR_PROPERTIES prop; pProperties-cBuffers 4; // 4帧缓冲足够平滑 pProperties-cbBuffer m_nFrameSize; // 单帧字节数 HRESULT hr pAlloc-SetProperties(pProperties, prop); return hr; }缓冲区数量和大小直接影响画面稳定性。如果cbBuffer小于一帧的字节数FillBuffer时GetPointer拿到的指针虽然能用但memcpy会越界写导致随机内存覆盖——这类问题极难排查不如一开始就按m_nFrameSize精确设置。4. 编译注册与踩坑排查从regsvr32到画面黑屏的完整链路4.1 编译环境与注册步骤VCAM是标准的Win32 DLL项目编译时把directshow的SDK包含目录指向Windows SDK里相应的include路径。用Visual Studio打开工程切到x86或x64平台选哪个取决于你的目标进程位数——32位软件必须注册32位DLL64位软件要注册64位DLL。生成成功后以管理员身份打开命令提示符执行注册regsvr32 VCAM.dll注册成功会弹出提示窗口。如果报错要么是DLL的依赖项缺失要么是权限不足。之后打开视频软件的设备列表就能看到名为VCAM Video的摄像头。4.2 验证输出画面用AMCap快速确认AMCap是个极简的DirectShow测试工具比打开QQ视频快得多。打开AMCapDevices菜单里选择VCAM如果画面立即显示你配置的图片说明整个链路通了。这一步能快速把问题定位到注册链路还是应用层协商。4.3 常见问题排查现象、原因、解决问题一regsvr32报错0x80070005现象注册命令执行后弹窗提示模块已加载但对DllRegisterServer的调用失败或直接拒绝访问。原因很直接——权限不够regsvr32需要写系统注册表HKLM\SOFTWARE\Classes节点。解决用管理员权限打开命令行重新执行。如果还失败检查VCAM.def文件里有没有导出DllRegisterServer缺了它注册入口都不存在。问题二设备列表有VCAM但画面黑屏现象AMCap能选中VCAM但画面全黑且无任何报错。原因通常是FillBuffer里拷贝的字节数大于缓冲区的实际大小或者m_pBmpData根本没加载成功但代码没做空指针保护数据拷了个寂寞。解决先在FillBuffer里加OutputDebugString打日志确认每帧是否被调用再检查图片路径是否是绝对路径配置文件里的路径存在与否直接影响加载结果。问题三画面比例不对或花屏现象视频画面被拉伸变形或者出现条纹。原因VIDEOINFOHEADER里的宽高和实际填充的像素数据尺寸不一致很多情况下图片不是RGB24格式用了RGB565的BMP加载进来直接拷给RGB24缓冲区字节序对不上。解决BMP加载后统一转换成24位DIB在DecideBufferSize里用转换后的m_nFrameSize并且在GetMediaType里保证宽高参数和真实数据一致。问题四有些软件找不到VCAM设备现象AMCap能选到但某个视频会议软件死活看不到。原因软件是64位进程注册的却是32位DLL反之亦然。解决编两套分别注册32位和64位版本或者至少确认目标软件的位数。还有一个隐蔽原因滤镜的CLSID没有正确写到HKLM\SOFTWARE\Classes\CLSID{GUID}\InprocServer32regsvr32虽然返回成功但注册表路径写错了用regedit核对一下。4.4 帧率控制的正确姿势很多人想控制输出帧率喜欢在FillBuffer里直接Sleep。这是个很容易翻车的手法——FillBuffer被调用时下游在阻塞等待数据Sleep会导致整条Graph卡顿看似帧率降了实际是CPU空转加延迟飙升。正确做法是控制时间戳间隔比如要25帧每秒就把m_dwFramePeriod设为400000单位100纳秒即40毫秒让渲染器按时间戳调度。5. 二次开发思路把静态图片换成动态视频流5.1 动态帧源接入视频文件循环播放VCAM最能直接改成实用的点就是把静态BMP换成一个视频播放器。思路不复杂用FFmpeg或OpenCV把视频解码成RGB帧FillBuffer里取下一帧。核心差异在时间戳同步——视频文件自带帧率你要以文件自身帧率为准而不是用固定m_dwFramePeriod。HRESULT CVCamStream::FillBuffer(IMediaSample* pSample) { BYTE* pData NULL; pSample-GetPointer(pData); if (m_pVideoCap m_pVideoCap-ReadNextFrame(pData)) { CRefTime tStart m_rtStart; CRefTime tStop m_rtStart m_frameDuration; pSample-SetTime(tStart, tStop); m_rtStart tStop; } return S_OK; }m_pVideoCap封装了OpenCV的VideoCapture读取能力ReadNextFrame把Mat数据拷贝到pData。要注意的是视频循环播放时文件读到头要seek到开始循环衔接处应该做一下黑白渐变处理不然画面会闪一下。5.2 动态切换画面源配置文件热加载把配置写到ini或json文件FillBuffer每次读一个全局变量指向当前画面源画面源切换就自然发生。我在项目里习惯加一个控制接口通过命名管道接收外部命令需要切换时写一行命令到管道过滤器内部开一个监听线程接收并更新资源。这样做的好处是上层UI和过滤器彻底解耦跑自动化测试时随时换源。5.3 兼容性扩展多格式支持前面提到很多软件默认请求YUY2格式可以在SetMediaType里增加一个YUY2分支在FillBuffer里加一个RGB24到YUY2的转换if (m_bFormatYUY2) { ConvertRGB24ToYUY2(pData, m_pBmpData, m_nWidth, m_nHeight); } else { memcpy(pData, m_pBmpData, m_nFrameSize); }转换函数参考标准的色彩空间矩阵实现就行注意YUY2的Y分量在前、UV分量交错排列横向像素必须成对处理奇数宽度图片要单独处理末尾像素。做这类虚拟设备最容易翻车的永远是缓冲区大小、像素格式、时间戳这三个点。从那以后我每次改完都强制走一遍完整流程重建DLL、重新注册、AMCap验证、日志确认、异常路径测试五步缺一不可。这套流程帮我避开过太多黑屏和花屏的坑了希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站