资讯动态

DirectShow虚拟摄像头VCAM技术拆解:过滤器、帧推送与二次开发

发布时间:2026/10/20 11:40:37 来源:尧图企业网站定制
简介面向DirectShow及视频开发者的虚拟摄像头实现资源基于DirectShow API构建VCAM可在没有物理摄像头时输出连续视频流适用于测试、演示或与第三方软件集成。压缩包共154个文件包含h与cpp源文件、ax组件、sln与vcxproj工程文件、obj及pdb编译产物另有log、txt等辅助说明总大小约71.33MB完整覆盖源过滤器创建、过滤图构建、Sample Grabber帧捕获、VMR视频渲染、系统设备注册等关键技术。已有2450人浏览学习。深入阅读源码可理解虚拟设备在Windows注册表中的枚举与注册机制掌握ICreateDevEnum、IGraphBuilder、IMediaControl等DirectShow核心接口的协作使用并借鉴多线程视频生成与性能优化经验。压缩包内含VCam.ax与VCamD.ax可注册组件源码支持Windows 10与Visual Studio 2015环境直接编译调试可作为虚拟摄像头二次开发、多媒体课程设计或DirectShow框架学习的有力参考。1. DirectShow 虚拟摄像头VCAM 能把什么画面变成摄像头当你在开会软件里只能选物理摄像头而你想把一张设计稿、一段程序渲染窗口甚至一个游戏画面当成“摄像头”信号喂给对方时问题就来了对方软件没有给你自定义视频源的入口。VCAM 这个项目就是为此存在的——它用 DirectShow 框架实现了一个虚拟摄像头过滤器安装注册之后会被系统摄像头枚举接口识别为普通摄像头画面内容由 VCAM 自己决定。典型使用场景是预设画面代替真人出镜、给视频流叠加 OSD、以及把任意画面源模拟成摄像头信号。适合想搞清 DirectShow 过滤器机制、或者需要给系统添加一个视频设备的开发者。项目体量不大但过滤器、引脚、推帧、注册的闭环完整。2. 技术底座DirectShow 过滤器图与虚拟视频源的三个关键点2.1 过滤器图源过滤器、变换过滤器与推式投递DirectShow 有一条固定认知任何能跑的媒体管线都是一张过滤器图。源过滤器负责产生数据变换过滤器负责处理数据渲染过滤器负责把数据送进播放器或编码器。三者各自实现 COM 接口由过滤图管理器把它们连起来调度。VCAM 在这张图里的身份是纯源过滤器——它不碰解码也不碰编码只负责按节奏把内存里的图像帧交给下游。懂了这个角色后面看实现就很顺。源过滤器分两种。拉式源是被动响应型的下游每次调 ReadSample 它才给一帧典型代表是文件阅读器推式源则是主动投递的——基类内部有个工作线程按媒体类型里写好的帧时长周期性地调用你的 FillBuffer 回调把数据推给下游。虚拟摄像头只能是推式源因为它没有硬件中断唯一能控制时序的就是它自己的代码。这解释了 VCAM 为什么要把 FillBuffer 做成一个独立于宿主调用的回调。再往下要清楚过滤图状态机。DirectShow 的过滤器有三个状态停止、暂停、运行。启动播放时图会先全体暂停、再全体运行暂停状态下推式源通常会投递一帧让下游尽快把缓冲占位。VCAM 的 FillBuffer 在暂停状态也会被调用这时候填充的帧会被下游缓存下来实际上会变成预览画面的第一帧。如果你在暂停时只填充了半帧或者干脆不填预览画面就会长期黑屏直到正式运行后的第二帧到达。我在调试时吃过这个亏后来固定做法是在 FillBuffer 里无条件填满整帧不区分暂停运行。实现层面基类库 CSourceStream 把上面这些状态机都接管了。我们只需要实现 GetMediaType、CheckMediaType、FillBuffer 三件事线程、时钟同步、缓冲管理全部由基类完成。这也是 DirectShow 虚拟摄像头项目之所以适合拿来学习的原因——框架帮你挡住八成复杂度你只需要理解“往里填帧”这一个动作。不想自己造轮子的话完全可以直接基于 CSourceStream 派生自己的引脚类省去大量重复的 COM 样板代码。2.2 让系统认出一个“假”摄像头CLSID 注册与设备枚举虚拟摄像头宣称自己是一个视频输入设备靠的不是名字也不是图标是注册表。上层应用枚举摄像头时DirectShow 的系统设备枚举器会扫描两类注册表位置收集候选 CLSID然后逐个实例化能查到 IBaseFilter 接口的就算数最终列出一个设备列表。VCAM 的安装过程本质上就是把它的 CLSID 写进这个扫描范围并让 DLL 的类工厂能正确实例化过滤器对象。要写的位置有两个。第一个是标准 COM 注册HKCR\CLSID{你的CLSID} 下的默认值和 InprocServer32这一层保证 CoCreateInstance 能找到 DLL 并得到类实例。第二个是设备类别注册HKLM\SYSTEM\CurrentControlSet\Control\MediaResources\DirectShow\Filter{你的CLSID}这一层决定系统在“视频捕获设备”枚举时会不会带上你。许多新手只做第一层、漏了第二层结果 regsvr32 成功、采集工具却找不到设备。把第二个位置叫“设备入场券”可能更好记CLSID 注册保证 COM 层面存在设备类别注册保证摄像头枚举层面可见。这里有个再往下才碰得到的细节枚举器是逐个实例化来挑过滤器的。如果你的 DllGetClassObject 实现有缺陷实例化抛错枚举器会把该 CLSID 静默跳过不报任何日志。所以排查“设备列表里没有 VCAM”时少在枚举器或采集工具上耗时间先写一段小测试程序直接对已注册 CLSID 调 CoCreateInstance能实例化成功再往上层排查。顺序反了很容易在你还没搞清是 COM 层还是枚举层出问题的时候就把时间浪费在调整工具参数上。2.3 两条实现路线共享内存桥接与纯过滤器方案市面上的虚拟摄像头大都走两条技术路线。一条是共享内存方案渲染程序把帧放进一块跨进程共享内存常驻后台的摄像头过滤器从共享内存里读帧再往下游推。主流直播软件的虚拟摄像头基本是这个结构好处是画面生产和设备投递分属两个进程互不拖累坏处是必须自己解决双缓冲同步、进程生命周期和内存权限代码量翻倍。另一条就是 VCAM 采用的纯过滤器方案帧源和投递逻辑在同一个进程内过滤器自己养一个帧源对象画面直接从内存地址拷进媒体样本。同进程带来的最大优势是链路短、延迟低——没有跨进程 IPC 的上下文切换和等待开销非常适合做帧同步、色彩测试这些精确控制的场景。代价是帧源生产如果卡了整个图像流立刻能看到这就要求帧源设计得足够轻。选哪条路线取决于你拿虚拟摄像头干吗。如果只是给会议软件提供一个稳定、无需实时合成的画面源共享内存方案的工程冗余反而值如果你想要的是对渲染帧做逐帧控制、叠加、替换、开环调试VCAM 这种同一进程的方案调试体验好得多依赖也少。我的建议是先从纯过滤器方案入手把 DirectShow 的数据流搞明白以后如果要改成共享内存也只多一层读取逻辑而已。3. 核心模块拆解VCAM 的过滤器、引脚与帧推送线程3.1 项目结构一个过滤器对象、一个输出 Pin 与帧缓冲池VCAM 的工程文件不复杂核心就四个编译单元VCam/ ├─ DllEntry.cpp // DllMain、DllRegisterServer、DllUnregisterServer ├─ VirtualCameraFilter.h/.cpp // 过滤器类继承 CSource创建引脚 ├─ VirtualCameraPin.h/.cpp // 输出引脚类继承 CSourceStream ├─ FrameSource.h/.cpp // 帧源接口 默认静态帧实现 └─ Resource.h // 版本资源DllEntry 决定 DLL 是否可以被 COM 系统合法使用里面三个导出函数各司其职。DllGetClassObject 返回类工厂DllCanUnloadNow 告诉系统 DLL 能不能卸载DllRegisterServer 和 DllUnregisterServer 负责写清注册表。类工厂是系统实例化过滤器的入口它内部必须对 CLSID 做匹配不是自己负责的 CLSID 就返回 CLASS_E_CLASSNOTAVAILABLE。很多坑都埋在这几个导出函数的实现里——比如把 DllGetClassObject 写错了注册表再对也实例化不出来。过滤器类 VirtualCameraFilter 做的事情非常单一在构造函数里创建输出引脚。因为这个过滤器永远只接下游、不接上游所以引脚只有一个类型是 CSourceStream 的派生。这个引脚同时承担两件事对外描述设备支持的图像格式对内作为推帧回调的宿主。你可以把 VirtualCameraPin 理解成一个源过滤器的嗓子——外部媒体类型协商是嘴型FillBuffer 是发声两个动作共用同一张嗓子所以也很容易出问题比如格式没定好就开始推帧。FrameSource 是 VCAM 特意抽象出来的一层。它声明了一个 GetFrame(BYTE*, size_t) 接口默认实现填充一张纯色静态图。真正的业务画面全部要经由 FrameSource 注入——这就给改造留了口子你想让这个摄像头显示什么只需要换一个 FrameSource 实现。帧缓冲池不多解释其实就是内部那两三个可复用的内存块避免每推一帧就 new/delete 一次拖慢 FillBuffer 的执行速度。3.2 媒体类型协商RGB32 与 YUY2 的选择过滤器接上下游之前必须先互相确认“我能生产什么格式、你需要什么格式”。这个确认过程叫媒体类型协商由引脚类的 GetMediaType 和 CheckMediaType 两个方法完成。GetMediaType 告诉下游我支持哪些格式CheckMediaType 是反向确认当图管理器尝试用某个格式连接时过滤器需要判断接不接受。看一段 GetMediaType 的实现// VirtualCameraPin.cpp —— 媒体类型枚举 HRESULT VirtualCameraPin::GetMediaType(int iPosition, CMediaType *pmt) { if (iPosition 0) return E_INVALIDARG; if (iPosition 1) return VFW_S_NO_MORE_ITEMS; // 目前只有 1 种格式 pmt-SetType(MEDIATYPE_Video); pmt-SetFormatType(FORMAT_VideoInfo); // 推荐先做 RGB32字节与像素一一对应调试时直接数内存就能定位颜色问题 pmt-SetSubtype(MEDIASUBTYPE_RGB32); pmt-SetSampleSize(1920 * 1080 * 4); // 写 VideoInfoHeader宽高、帧率、比特率 VIDEOINFOHEADER *vih reinterpret_castVIDEOINFOHEADER*(pmt-AllocFormatBuffer(sizeof(VIDEOINFOHEADER))); ZeroMemory(vih, sizeof(VIDEOINFOHEADER)); vih-bmiHeader.biWidth 1920; vih-bmiHeader.biHeight 1080; vih-bmiHeader.biBitCount 32; vih-bmiHeader.biSizeImage 1920 * 1080 * 4; vih-AvgTimePerFrame 333333; // 30fps1 秒换算成 100ns 单位 return S_OK; }这段代码做的事情全围绕一件事让下游在建立连接时拿到明确的帧格式。iPosition 是索引把格式列表暴露给下游轮询VFW_S_NO_MORE_ITEMS 表示列表到底了。VideoInfoHeader 里的 biHeight 也是容易踩的细节负数代表从上到下存储正数代表倒置的 BMP 顺序大多数 DirectShow 视频帧习惯用倒置存储如果你的帧源拷入内存是按正向顺序画面会出现上下颠倒。CheckMediaType 的典型实现是先判断 pmt-majortype 是不是 MEDIATYPE_Video再对比 subtype 是不是 MEDIASUBTYPE_RGB32一样就返回 S_OK不一样返回 S_FALSE。这相当于面试官先问“你会不会这个格式”答不会就直接拒掉图管理器会尝试别的搭配直到双方都点头连接才建立。这套协商看着繁琐却是保证下游播放器、编码器均匀消费帧的基础。3.3 帧推送线程sleep 控制帧率与参考时钟校准推帧的物理入口是 CSourceStream 的 FillBuffer基类负责缓冲分配我们只做填充和写时间戳。实现大致这样// VirtualCameraPin.cpp —— 填充一帧到媒体样本 HRESULT VirtualCameraPin::FillBuffer(IMediaSample *pSample) { BYTE *pData nullptr; pSample-GetPointer(pData); FrameSource *src GetFrameSource(); // 从帧源拉取最新画面 src-GetFrame(pData, m_videoWidth * m_videoHeight * 4); pSample-SetActualDataLength(m_videoWidth * m_videoHeight * 4); // 累加式时间戳从 0 开始线性递增避免依赖系统时钟导致时序抖动 REFERENCE_TIME rtStart m_rtStart; REFERENCE_TIME rtStop m_rtStart m_rtFrameDuration; pSample-SetTime(rtStart, rtStop); pSample-SetMediaTime(rtStart, rtStop); m_rtStart rtStop; return S_OK; }FillBuffer 每调用一次就是交付一帧。基类按 VideoInfoHeader 里的 AvgTimePerFrame 来决定下一次调用的周期不需要自己 sleep——这点很容易做反好多初次写过滤器的人在 FillBuffer 里加 Sleep 去控帧率结果每次投递延时叠加帧率反而越来越低。真正的帧率控制权在媒体类型协商时就已经交给基类了你只需要保证这次 FillBuffer 在预计时间内完成。写时间戳也不是随便填。常见误区是把系统当前时间直接塞进样本问题在于下游播放器需要的是连续的、可预期的时序而系统时钟带调度抖动。累加式时间戳从第一帧 0 开始每一帧固定增加 3333331/30 秒的 100ns 单位即使实际投递间隔有些波动下游看到的仍然是一条严格的匀速时间轴。这会让会议软件里的画面观感明显更顺。3.4 客户端如何拿到画面帧回调接口与生产者消费者模型虚拟摄像头自己不会主动联系客户端但它对外暴露了两个能插入业务的点。一个点在接受下游环节的“拉”请求时任何采集软件通过媒体样本取走帧就可以另一个点是宿主程序向 VCAM 注入画面时——通过 FrameSource 接口实现。把这两个点说清楚就看清了整个生产消费模型。常见做法是给过滤器实现 IAMStreamConfig 接口让外部应用能够查询和修改输出分辨率。IAMStreamConfig 提供 GetStreamCaps 和 SetFormat 两个方法前者和 GetMediaType 一样是枚举格式列表后者允许在运行时切换分辨率。很多采集工具在设置页会枚举这个接口来显示分辨率下拉框。如果 VCAM 不实现这个接口工具也能工作只会在默认格式下推流所以它不是必要的而是锦上添花。帧的注入路径用一段宿主代码来说// 宿主侧把自定义画面注入 VCAM 的帧源 class MyFrameSource : public FrameSource { public: HRESULT GetFrame(BYTE *dst, size_t size) override { // 从渲染纹理或窗口位图抓取像素拷入推帧缓冲 CopyMemory(dst, m_renderBuffer, size); return S_OK; } };从宿主侧看这就是生产者消费者模型宿主是生产者FillBuffer 线程是消费者。生产端只要保证 GetFrame 被调用时数据已经画好消费端则在基类时钟驱动下周期性地调 GetFrame。这里最常犯的错误是生产速率低于消费速率——渲染一帧要 50msFillBuffer 每 33ms 就来要一次强读半成品就会闪现撕裂。解决路径前面说过FrameSource 内部维护双缓冲渲染线程只画备缓冲GetFrame 只交换主缓冲二者互不干扰。把全链路串起来的顺序是外部程序枚举设备 → CoCreateInstance 实例化 → 查询 IBaseFilter 和输出引脚 → 媒体类型协商 → 图进入运行状态 → 基类周期性调 FillBuffer → VCAM 从 FrameSource 取帧 → 交付下游。所有这些对宿主程序透明宿主只需要明白自己写的是一个“填充函数”。理解这条链路决定在哪里下手做二次开发就很清晰了。4. 编译、注册、联调与避坑五条常见失败案例逐个拆4.1 编译环境Windows SDK、DirectShow 基类库与平台工具集DirectShow 不在独立 SDK 里Windows SDK 自带头文件和文档安装最新 SDK 就能拿到 dshow.h、dsound.h 这些头文件。要额外处理的是基类库 strmbase——它没有提供现成 lib源码放在安装目录的 Samples\Multimedia\DirectShow\BaseClasses 下需要先单独编译一遍生成 strmbase.lib再在 VCAM 工程里链接它。注意 SDK 自带的工程可能是老 VS 格式我会直接新建一个 VS 静态库项目把 BaseClasses 里的 .cpp 全部拖进去重新编输出 strmbase.lib这样比去修老工程格式省事。一个容易被忽略的依赖是 uuid 库和 ole32。DirectShow 的接口类型全在 uuid 里代码里会直接引用 CLSID_FilterGraph、IID_IAMStreamConfig 这些符号链接时要加上 strmiids.lib。少了它会报一堆 unresolved external symbol全是对接口 GUID 的引用但实际不是代码问题就是少链接了库。另一个常见链接错误还是 BaseClasses 自身的它的某些源文件在 VS2017 以上版本默认启用的安全异常处理下会链接失败表现是 LNK2028/LNK2019 混合出现解决办法是给 strmbase 工程加上 /SAFESEH:NO。位数选择我建议双平台都编32 位版本优先。原因在于兼容一批老牌采集程序仍是 32 位进程它们通过 WoW64 重定向访问注册表读不到 64 位版本的设备注册项摄像头列表里就会缺项。如果你在预览工具里怎么都正常、在 32 位调用方里失踪先查位数再查代码。4.2 注册三步regsvr32 之前先改注册表注册 DLL 的命令非常简单# 管理员权限下执行否则写 HKLM 会静默失败 regsvr32 /s VCam.dll但 regsvr32 的返回码太不靠谱。注册前先确认两件事DLL 依赖的库要么静态链接进去、要么放在同目录避免注册时 COM 加载依赖失败第二是“以管理员身份运行”命令提示符因为 HKLM 需要管理员写权限普通权限下 regsvr32 常常显示操作完成实际没写进去。注册完我会顺手用 reg query 验证而不是只看返回码reg query HKCR\CLSID\{你的CLSID}\InprocServer32 /ve reg query HKLM\SYSTEM\CurrentControlSet\Control\MediaResources\DirectShow\Filter /s /f {你的CLSID}第一条命令验证 COM 实例化入口第二条验证设备类别枚举。两条都能查到才说明“设备入场券”已生效。DllRegisterServer 函数里应该把这两处写入都做掉卸载时对应 DllUnregisterServer 对称删干净避免残留注册表项影响后续调试。4.3 设备枚举验证采集预览工具里看到 Vcam 才算过注册完成后用带设备枚举能力的采集预览工具打开视频输入设备列表VCAM 应该出现在“视频捕获设备”分组。选它并开启预览这一瞬间工具实际执行了实例化、连接、媒体类型协商、开始推帧一整条链路。能走到这一步说明注册表和 COM 层都通了。列表里看不到的按第 4.2 节的两条注册表路径再查一遍别在工具上折腾。能看到却黑屏多半出在媒体类型协商。许多采集工具会先尝试 YUY2 这类 YUV 格式如果你的 GetMediaType 只给了 RGB32协商就会失败。此时工具要么重试另一种格式要么直接停在黑屏。解决方式是在 GetMediaType 里按顺序提供 RGB32 和 YUY2 两种格式FillBuffer 再按实际协商到的格式填帧。我一般把格式判断写在 FillBuffer 开头根据当前协商出来的 subtype 分支填 RGB 还是 YUV 数据时刻记得使用中的格式可能不是默认那一个。验证这步值得做成脚本别手点。用一个 bat 串起“注册 DLL → 启动预览工具 → 自动录制 10 秒 → 停用设备”每次改完代码一键回归。手动操作一来慢二来容易漏改状态——比如设备被上一个工具占用没释放重新注册后新实例化失败脚本能顺便暴露这种状态残留问题。4.4 五条常见失败案例案例一regsvr32 提示成功采集工具里看不到设备。现象是注册函数返回正常但任意工具的摄像头列表都没有 VCAM。原因是 DllRegisterServer 只写了 COM 注册项漏了 DirectShow\Filter 设备类别项枚举器扫描时压根不带它。解决方法是补全设备类别注册项代码里用一个 RegisterFilterCategory 辅助函数统一写这里卸载时对称清理。案例二32 位应用看不到设备64 位应用正常。现象是打包工具、老采集程序里找不到 VCAM但新版预览工具能看到。原因是 WoW64 重定向32 位进程访问 HKLM\SYSTEM 下的注册表时被系统重定向到 WOW6432Node 分支VCAM 只写了原生分支32 位进程自然看不到。解决方法是把注册逻辑做成双写原生分支和 WOW6432Node 分支各写一份或者干脆分别给 32 位和 64 位做一套注册安装流程。案例三预览画面黑屏过滤器无报错。现象是 FillBuffer 正常调用、时间戳正常画面仍是全黑。原因是帧缓冲从未清零初始化且填充时调用了 SetActualDataLength 但数据长度比缓冲小下游只取实际长度那一段黑的部分是残留的未知内存。解决方法是创建媒体样本时就 ZeroMemory 一遍并在 FillBuffer 末尾严格按帧大小 SetActualDataLength。案例四帧率固定在 30但画面周期性抖动。现象是录制工具显示帧率正确肉眼看到画面每隔几秒抽一下。原因是帧源合成耗时偶尔超过一帧时长基类会自动跳帧追时间表现为抽搐。解决方法是把图像合成改成异步预合成并且在 FrameSource 里加“来不及就复用上一帧”的兜底判断避免一帧卡死连累整条流。案例五设备被占用下一个程序没法复用。现象是把 VCAM 从一个采集工具切到另一个时新工具报设备已被占用。原因是上一个工具的过滤图没有正确停止并释放引脚。解决方法是确认过滤图 Stop 后缓冲确实被释放——在 DllCanUnloadNow 的逻辑里做引用计数保护别让 DLL 在设备仍被占用时被系统卸载同时排障时养成习惯先关掉所有占用方再重新注册。这五条从注册表、枚举、协商、填帧到资源释放都覆盖了。排障顺序上记住一句话注册表链 → 枚举 → 媒体类型协商 → 填帧 → 释放顺着走大多数问题半小时内能定位到具体环节。5. 二次开发实战把静态帧改成动态渲染源5.1 第一步替换帧源让画面来自你的渲染代码FrameSource 默认填充的是静态纯色图工程跑起来后看到的也是固定画面。把它改成动态画面源只需重写 FrameSource 的派生你的渲染线程负责往备缓冲画东西GetFrame 负责交换主缓冲给 FillBuffer 用。最先要注意的是给这两个缓冲加锁——最简单用临界区把 GetFrame 内的拷贝包起来渲染线程在另一侧对备缓冲加锁绘图。改完先别急着接上层工具用一个灰度测试确认帧源没问题在 GetFrame 里每帧往缓冲写一行递增的颜色条纹预览时看到条纹在动说明你的帧源已经生效后面换什么都顺理成章。5.2 第二步时间戳修正与帧率适配如果你注入的动态源帧率不是固定 30fps需要处理帧率适配。常见的取巧做法是在 FillBuffer 里临时改 AvgTimePerFrame但 CSourceStream 在运行中并不读这个字段改了没用。可行的适配方案是引入一个输出调度器FrameSource 每推入一帧调度器计算该帧的实际到达时间再向 FillBuffer 输出对应的媒体时间戳。发慢了就重复上一帧把时间轴补住发快了就丢帧保持节奏。注意动态源的节奏跑偏常常不是帧率本身的问题而是下游的声画同步。会议软件在音频正常、视频偶尔快慢的时候会认为摄像头数据流不稳定屏幕出现缓冲或花屏。这就是为什么要保持一个相对稳定的 25fps 或 30fps——宁可丢帧丢合成精度也不让时间轴上出现大的跳变。5.3 第三步用虚拟摄像头跑通一个完整闭环改造完成后按三段走验证闭环。第一段用采集预览工具看静态画面是否正常第二段用录屏功能录制 30 秒动态画面回放确认流畅且无撕裂第三段打开会议软件把 VCAM 设为唯一摄像头观察对端接收稳定继续跑 2 分钟确认没有断流。三段全过这次改动才算真的落得了地。跑完整闭环时顺手把第 4.3 的测试脚本补上动态画面用例以后每次改动都一键回归。这是我栽过跟头之后养成的习惯——第一回改造只做了静态验证上线到会议软件里才发现时间戳散乱被对方反馈画面像幻灯片。从那以后我每次改完虚拟摄像头代码都强制走一遍“静态快照 → 动态预录 → 会议软件联调”三段式闭环过了才提交。希望这篇拆解能帮你把 VCAM 吃透做二次开发的时候少绕几圈。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑