今天我们要实现一个虚拟 Camera 驱动。有这个驱动,在 播放软件(如 VLC)、视频会议软件、主播视频制作软件(如 OBS)中,就可以播放、加入我们的各种特制内容了。

先看看实现后的效果:

在 OBS 中使用我们的 Camera:

在 Vlc 中播放使用我们的 Camera:

主要实现步骤

说是驱动,其实与真正的物理摄像头驱动是不一样的。我们买的物理摄像头,是通过 USB 与电脑连接,使用 UVC(USB Video Class)规范实现。

在 Windows 平台,实现虚拟 Camera,更简单的方法是基于 DirectShow 实现一个应用层的 Capture Source Filter,而且大部分 Windows 平台的视频软件都会适配 DirectShow Capture。

这篇文章假定你已经了解 DirectShow 的基本框架、工作原理,并且有一点的实践经验。在此基础上,通过这篇文章,能够了解到虚拟 Camera 的实现必要工作,并通过下面的基本步骤,可以完成一个真正的可以工作的虚拟 Camera。

通过实践总结下来,实现虚拟 Camera 需要以下几步:

  • 实现 IMediaFilter、IPin,实现基本的Pin 管理,图像输出
  • 实现 IKsPropertySet,声明 Pin 的类型(Capture、Preview)
  • 实现 IAMStreamConfig,支持 Camera 配置,如分辨率,帧率
  • 实现 IPropertyPage,支持配置的 Sheet(对话框),比如输入虚拟数据源的地址
  • 实现 ISpecifyPropertyPages,对外声明,本 Filter 支持的配置的 Sheet
  • 实现 Capture 的注册,注册为 Camera 设备,让其他软件能够找到你

实现 IMediaFilter、IPin

实现 IMediaFilter、IPin,是实现 DirectShow Source Filter 的基本任务。可以参考我的另外两篇文章:

播放器插件实现系列 —— DirectShow 之 SourceFilter_Fighting Horse的博客-CSDN博客

基于 DirectShow 实现 SourceFilter 常见问题分析_Fighting Horse的博客-CSDN博客

需要说明的是,Camera 中可用的视频格式是有限的。除了未压缩的 RGB、YUV 格式,只支持 MJPG 格式。这是行业的常规标准,也是出于成本考虑,支持视频编码的摄像头肯定要贵一些。

因此如果输入源是视频文件(一般是 H264 编码),想要虚拟为摄像头,就要考虑其他方案了,否则使用 Camera 的软件基本上用不了你的 Camera。

实现 IKsPropertySet

通过接口 IKsPropertySet,声明 Pin 是 CAPTURE 类型的。

接口 IKsPropertySet 有三个方法:

Method Description
Get Retrieves a property identified by a property set GUID and a property ID.
QuerySupported Determines whether an object supports a specified property set.
Set Sets a property identified by a property set GUID and a property ID.

不支持任何设置操作:

// Set: Cannot set any properties.
HRESULT CMyCapturePin::Set(REFGUID guidPropSet, DWORD dwID,void *pInstanceData, DWORD cbInstanceData, void *pPropData, DWORD cbPropData)
{return E_NOTIMPL;
}

只支持获取 catagory 属性:

// Get: Return the pin category (our only property).
HRESULT CMyCapturePin::Get(REFGUID guidPropSet,   // Which property set.DWORD dwPropID,        // Which property in that set.void *pInstanceData,   // Instance data (ignore).DWORD cbInstanceData,  // Size of the instance data (ignore).void *pPropData,       // Buffer to receive the property data.DWORD cbPropData,      // Size of the buffer.DWORD *pcbReturned     // Return the size of the property.
)
{if (guidPropSet != AMPROPSETID_Pin) return E_PROP_SET_UNSUPPORTED;if (dwPropID != AMPROPERTY_PIN_CATEGORY)return E_PROP_ID_UNSUPPORTED;if (pPropData == NULL && pcbReturned == NULL)return E_POINTER;if (pcbReturned)*pcbReturned = sizeof(GUID);if (pPropData == NULL)  // Caller just wants to know the size.return S_OK;if (cbPropData < sizeof(GUID)) // The buffer is too small.return E_UNEXPECTED;*(GUID *)pPropData = PIN_CATEGORY_CAPTURE;return S_OK;
}

还是只支持 CATAGORY 属性,只读:

// QuerySupported: Query whether the pin supports the specified property.
HRESULT CMyCapturePin::QuerySupported(REFGUID guidPropSet, DWORD dwPropID,DWORD *pTypeSupport)
{if (guidPropSet != AMPROPSETID_Pin)return E_PROP_SET_UNSUPPORTED;if (dwPropID != AMPROPERTY_PIN_CATEGORY)return E_PROP_ID_UNSUPPORTED;if (pTypeSupport)// We support getting this property, but not setting it.*pTypeSupport = KSPROPERTY_SUPPORT_GET; return S_OK;
}

实现 Capture 的注册

只是声明 Pin 的类型,并不能让其他应用觉得你是一个 Camera。这比较令人泄气,毕竟没有什么比在其他应用中的看到我们的存在更令人兴奋了。

所以这一步是很关键的,当完成这一步之后,我们可以在其他应用中可以间接的操作我们的 Camera,调试我们的代码。

与一般 DirectShow Filter 的注册不一样的是,Capture Filter 还需要注册到 VideoInputDeviceCategory 中。

IFilterMapper2* fm = 0;
hr = CreateComObject(CLSID_FilterMapper2, IID_IFilterMapper2, fm);
if (SUCCEEDED(hr))
{if (bRegister){IMoniker* pMoniker = 0;REGFILTER2 rf2;rf2.dwVersion = 1;rf2.dwMerit = MERIT_DO_NOT_USE;rf2.cPins = 1;rf2.rgPins = sudMyPin;// this is the name that actually shows up in VLC et al. weirdhr = fm->RegisterFilter(CLSID_MyCamera, g_wszMyCamera, &pMoniker, &CLSID_VideoInputDeviceCategory, NULL, &rf2);pMoniker->Release();}else{hr = fm->UnregisterFilter(&CLSID_VideoInputDeviceCategory, 0, CLSID_MyCamera);}
}// release interface
//
if (fm)fm->Release();

从注册表中,可以找到注册的结果:

实现 IAMStreamConfig

通过接口 IAMStreamConfig,对外暴露图像格式的细节。与 IPin::EnumMediaTypes 不同,这里给出的是各种配置参数的范围、可选值,也支持配置各种参数的值。

IAMStreamConfig::GetFormat
The GetFormat method retrieves the current or preferred output format.
IAMStreamConfig::GetNumberOfCapabilities
The GetNumberOfCapabilities method retrieves the number of format capabilities that this pin supports.
IAMStreamConfig::GetStreamCaps
The GetStreamCaps method retrieves a set of format capabilities.
IAMStreamConfig::SetFormat
The SetFormat method sets the output format on the pin.
HRESULT STDMETHODCALLTYPE CMyCapturePin::GetNumberOfCapabilities(int* piCount, int* piSize)
{*piCount = 1;*piSize = sizeof(VIDEO_STREAM_CONFIG_CAPS); // VIDEO_STREAM_CONFIG_CAPS is an MS structreturn S_OK;
}

外部获取各种配置参数的范围、可选值

HRESULT STDMETHODCALLTYPE CMyCapturePin::GetStreamCaps(int iIndex, AM_MEDIA_TYPE** pmt, BYTE* pSCC)
{CAutoLock cAutoLock(m_pFilter->pStateLock());HRESULT hr = GetMediaType(&m_mt); // setup then re-use m_mt ... why not?// some are indeed shared, apparently.if (FAILED(hr)){return hr;}*pmt = CreateMediaType(&m_mt); // a windows lib method, also does a copy for usif (*pmt == NULL) return E_OUTOFMEMORY;DECLARE_PTR(VIDEO_STREAM_CONFIG_CAPS, pvscc, pSCC);/*most of these are listed as deprecated by msdn... yet some still used, apparently. odd.*/pvscc->VideoStandard = AnalogVideo_None;pvscc->InputSize.cx = m_info->format.video.width;pvscc->InputSize.cy = m_info->format.video.height;// most of these values are fakes..pvscc->MinCroppingSize.cx = m_info->format.video.width;pvscc->MinCroppingSize.cy = m_info->format.video.height;pvscc->MaxCroppingSize.cx = m_info->format.video.width;pvscc->MaxCroppingSize.cy = m_info->format.video.height;pvscc->CropGranularityX = 1;pvscc->CropGranularityY = 1;pvscc->CropAlignX = 1;pvscc->CropAlignY = 1;pvscc->MinOutputSize.cx = m_info->format.video.width;pvscc->MinOutputSize.cy = m_info->format.video.height;pvscc->MaxOutputSize.cx = m_info->format.video.width;pvscc->MaxOutputSize.cy = m_info->format.video.height;pvscc->OutputGranularityX = 1;pvscc->OutputGranularityY = 1;pvscc->StretchTapsX = 1; // We do 1 tap. I guess...pvscc->StretchTapsY = 1;pvscc->ShrinkTapsX = 1;pvscc->ShrinkTapsY = 1;pvscc->MinFrameInterval = 500000; // the larger default is actually the MinFrameInterval, not the maxpvscc->MaxFrameInterval = 500000000; // 0.02 fps :) [though it could go lower, really...]pvscc->MinBitsPerSecond = (LONG)1 * 1 * 8 * m_info->format.video.frame_rate; // if in 8 bit mode 1x1. I guess.pvscc->MaxBitsPerSecond = (LONG)m_info->format.video.width * m_info->format.video.height * 32 * m_info->format.video.frame_rate + 44; // + 44 header size? + the palette?return hr;
}

外部获取当前媒体格式:

HRESULT STDMETHODCALLTYPE CMyCapturePin::GetFormat(AM_MEDIA_TYPE** ppmt)
{CAutoLock cAutoLock(m_pFilter->pStateLock());if (!m_bFormatAlreadySet) {HRESULT hr = GetMediaType(&m_mt); // setup with index "0" kind of the default/preferred...I guess...if (FAILED(hr)){return hr;}}*ppmt = CreateMediaType(&m_mt); // windows internal method, also does copyreturn S_OK;
}

外部配置媒体格式:

HRESULT STDMETHODCALLTYPE CMyCapturePin::SetFormat(AM_MEDIA_TYPE* pmt)
{CAutoLock cAutoLock(m_pFilter->pStateLock());// I *think* it can go back and forth, then.  You can call GetStreamCaps to enumerate, then call// SetFormat, then later calls to GetMediaType/GetStreamCaps/EnumMediatypes will all "have" to just give this one// though theoretically they could also call EnumMediaTypes, then Set MediaType, and not call SetFormat// does flash call both? what order for flash/ffmpeg/vlc calling both?// LODO update msdn// "they" [can] call this...see msdn for SetFormat// NULL means reset to default type...if (pmt != NULL){if (pmt->formattype != FORMAT_VideoInfo)  // FORMAT_VideoInfo == {CLSID_KsDataTypeHandlerVideo} return E_FAIL;// LODO I should do more here...http://msdn.microsoft.com/en-us/library/dd319788.aspx I guess [meh]// LODO should fail if we're already streaming... [?]if (CheckMediaType((CMediaType*)pmt) != S_OK) {return E_FAIL; // just in case :P [FME...]}VIDEOINFOHEADER* pvi = (VIDEOINFOHEADER*)pmt->pbFormat;// for FMLE's benefit, only accept a setFormat of our "final" width [force setting via registry I guess, otherwise it only shows 80x60 whoa!]     // flash media live encoder uses setFormat to determine widths [?] and then only displays the smallest? huh?if (pvi->bmiHeader.biWidth != m_info->format.video.width ||pvi->bmiHeader.biHeight != m_info->format.video.height){return E_INVALIDARG;}// ignore other things like cropping requests for now...// now save it away...for being able to re-offer it later. We could use Set MediaType but we're just being lazy and re-using m_mt for many things I guessm_mt = *pmt;}IPin* pin;ConnectedTo(&pin);if (pin){IFilterGraph* pGraph = m_pFilter->GetFilterGraph();HRESULT res = pGraph->Reconnect(this);if (res != S_OK) // LODO check first, and then just re-use the old one?return res; // else return early...not really sure how to handle this...since we already set m_mt...but it's a pretty rare case I think...// plus ours is a weird case...}else {// graph hasn't been built yet...// so we're ok with "whatever" format they pass us, we're just in the setup phase...}// success of some typeif (pmt == NULL) {m_bFormatAlreadySet = FALSE;}else {m_bFormatAlreadySet = TRUE;}return S_OK;
}

实现 IPropertyPage

通过 IPropertyPage 提供自定义的 Camera 配置或者信息展示的 UI 页面,其他应用也可以给用户展示该页面。

对于虚拟 Camera 来说,自定义的配置的最大用处是让用户输入图像数据的来源。比如将一个视频文件虚拟为 Camera,那么就要做一个 UI 界面,让用户选择他的视频文件。这个工作就在这一步完成。

如上图,这里我们只实现了一个输入框。

DirectShow baseclasses 提供了 CPropertyPage 类帮助实现 IPropertyPage,我们只需要实现下列方法,就可以工作了:

virtual HRESULT OnConnect(IUnknown* pUnk);
virtual HRESULT OnActivate();
virtual INT_PTR OnReceiveMessage(HWND hwnd,UINT uMsg, WPARAM wParam, LPARAM lParam);
virtual HRESULT OnApplyChanges();
virtual HRESULT OnDisconnect();

不过,还需要我们自己添加对话框资源,开发过 MFC 界面程序的程序员,应该都知道。不知道也很简单,通过拖拽一些控件就能够完成了。需要说明的是,新建对话框时,选择 IDD_OLE_PROPPAGE_SMALL。

在进一步实现 CPropertyPage 前,还需要定义并实现自己的读写配置值的接口:

DEFINE_GUID(IID_ICameraConfig,0x608b220, 0xe2f8, 0x4ddb, 0x99, 0xb6, 0xbf, 0xe5, 0x54, 0x25, 0xa9, 0xee);interface ICameraConfig : public IUnknown
{STDMETHOD(GetUrl)(LPCTSTR* psUrl) = 0;STDMETHOD(SetUrl)(LPCTSTR sUrl) = 0;
};

实现该接口:

STDMETHODIMP_(HRESULT __stdcall) CMyCamera::GetUrl(LPCTSTR* psUrl)
{*psUrl = m_URL;return S_OK;
}STDMETHODIMP_(HRESULT __stdcall) CMyCamera::SetUrl(LPCTSTR sUrl)
{lstrcpyW(m_URL, sUrl);Load(m_URL, NULL);return S_OK;
}

接下来就是实现 CPropertyPage 的几个方法了:

在连接时,查询并保存配置接口 ICameraConfig 对象:

HRESULT CMyPropertyPage::OnConnect(IUnknown* pUnk)
{if (pUnk == NULL){return E_POINTER;}ASSERT(m_pConfig == NULL);return pUnk->QueryInterface(IID_ICameraConfig,reinterpret_cast<void**>(&m_pConfig));
}

在激活时,对话框窗口已经创建了,可以填入当前的配置值:

HRESULT CMyPropertyPage::OnActivate()
{ASSERT(m_pConfig != NULL);LPCTSTR url;HRESULT hr = m_pConfig->GetUrl(&url);if (SUCCEEDED(hr)){SendDlgItemMessage(m_Dlg, IDC_URL, WM_SETTEXT, 0, (LPARAM)url);}return hr;
}

在收到 Windows 消息时,比如文本框文字改变时,标记配置值被修改了:

INT_PTR CMyPropertyPage::OnReceiveMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{switch (uMsg){case WM_COMMAND:if (wParam == MAKEWPARAM(IDC_URL, EN_CHANGE)) {if (IsWindowVisible(m_hwnd))SetDirty();}break;} // Switch.// Let the parent class handle the message.return CBasePropertyPage::OnReceiveMessage(hwnd, uMsg, wParam, lParam);
}void CMyPropertyPage::SetDirty()
{m_bDirty = TRUE;if (m_pPageSite){m_pPageSite->OnStatusChange(PROPPAGESTATUS_DIRTY);}
}

在用户点击“确认”或者"应用" 时,写入新的配置值:

HRESULT CMyPropertyPage::OnApplyChanges() {ASSERT(m_pConfig != NULL);TCHAR url[MAX_PATH];SendDlgItemMessage(m_Dlg, IDC_URL, WM_GETTEXT, MAX_PATH, (LPARAM)url);HRESULT hr = m_pConfig->SetUrl(url);return hr;
}

在断开连接时,释放配置接口 ICameraConfig 对象:

HRESULT CMyPropertyPage::OnDisconnect()
{if (m_pConfig){m_pConfig->Release();m_pConfig = NULL;}return S_OK;
}

实现 ISpecifyPropertyPages

只有 IPropertyPage 并没有展示出配置界面。还需要实现 ISpecifyPropertyPages 接口。该接口只有一个方法:

ISpecifyPropertyPages::GetPages
Retrieves a list of property pages that can be displayed in this object's property sheet.

该方法返回一个 GUID 数组,但是应该返回什么 GUID,文档中说得很模糊。

在尝试了很久之后,才明白,需要将上面的 IPropertyPage 对象像 DirectShow Filter 一样注册,然后在这里返回对应的 CLSID。

注册 CMyPropertyPage:

CFactoryTemplate g_Templates[] =
{......,{L"My Camera Property Page",& CLSID_MyCameraPropertyPage,CMyPropertyPage::CreateInstance,NULL,NULL}
};

实现 GetPages 方法:

STDMETHODIMP_(HRESULT __stdcall) CMyCamera::GetPages(CAUUID* pPages)
{pPages->cElems = 1;pPages->pElems = (GUID*)CoTaskMemAlloc(sizeof(GUID));if (pPages->pElems == NULL){return E_OUTOFMEMORY;}pPages->pElems[0] = CLSID_MyCameraPropertyPage;return S_OK;
}

实现 DirectShow 虚拟 Camera 驱动相关推荐

  1. 7.camera驱动06-自己实现v4l2驱动-虚拟摄像头

    1. 框架分层 实际上的v4l2框架: v4l2本质是还是一个字符设备驱动,有自己的fops. 每注册一个video_device都会以次设备号为下标放到v4l2层的一个数组里. 应用调用open函数 ...

  2. Android MTK Camera驱动代码分析

    一.Camera调用过程:      imgsensor起到承上启下的作用,在系统起来时会创建整个camera驱动运行的环境,其中主要的文件和函数如下框图所示,先设备挂载时会调用注册platform设 ...

  3. 虚拟摄像头驱动原理及开发

    (以下所说的都是基于微软的windows平台)                类似功能的产品,如著名的e2eSoft的 VCam,国内新浪的9518虚拟视频, 新浪的虚拟视频是DirectShow应用 ...

  4. camera驱动开机加载流程

    camera驱动 图片不好上传,可以跟着源码去看,MT6735下面的 也可以查看有道云链接:点击打开链接 一.模块加载函数 模块加载函数位于kd_sensorlist.c文件中,kd_sensorli ...

  5. 【Camera专题】Camera驱动源码全解析_下

    系列文章 1.手把手撸一份驱动 到 点亮 Camera 2.Camera dtsi 完全解析 3.Camera驱动源码全解析上 4.Camera驱动源码全解析下 上篇文章分析了C文件函数的实现,本文继 ...

  6. mtk android tv软件架构,MTK 平台Camera 驱动架构

    Platform_driver 这个结构体包含 Probe(). Remove()等函数来完成驱动的填充. b)设备的注册: 对 platform_device 的定义通常在 BSP 的板级文件( k ...

  7. 【高通SDM660平台】(1) --- Camera 驱动 Bringup Guide

    [高通SDM660平台]Camera 驱动 Bringup Guide 一.Kernel 代码移植 1. DTS 文件配置 1.1 sdm660.dtsi 1.2 sdm660-camera.dtsi ...

  8. 服务器如何安装虚拟声卡,虚拟声卡驱动安装步骤_虚拟声卡驱动有什么使用要求...

    这音频设备应用过程中经常是需要使用一些虚拟声卡驱动软件的,因为只有有了这种软件的支持,对于没有内录功能的笔记本电脑也是特别有用的一种工具.使用虚拟声卡驱动软件过程中,非常方便的让用户用来架设虚拟的线路 ...

  9. Android高通平台调试Camera驱动全纪录

    项目比较紧,3周内把一个带有外置ISP,MIPI数据通信,800万像素的camera从无驱动到实现客户全部需求. 1日 搭平台,建环境,编译内核,烧写代码. 我是一直在Window下搭个虚拟机登服务器 ...

最新文章

  1. ajax的loading方法,Ajax加载中显示loading的方法
  2. 比尔盖茨NEJM发文:新冠肺炎是百年一遇的流行病!全世界应该如何应对?
  3. 终于!《iOS 全埋点解决方案》正式出版
  4. Vscode html代码快速填写
  5. 深度学习之基于DCGAN实现手写数字生成
  6. html script 放置位置,script标签应该放在HTML哪里,总结分享
  7. 信息学奥赛一本通C++语言——1112:最大值和最小值的差
  8. Linux学习总结(64)——DBA常用的Linux命令汇总
  9. 阶段1 语言基础+高级_1-2 -面向对象和封装_1面向对象思想的概述
  10. java --map遍历
  11. 009-2010网络最热的 嵌入式学习|ARM|Linux|wince|ucos|经典资料与实例分析
  12. Visual Studio Code 取色器插件 取色选取 插件安装和使用
  13. Linux 句柄是什么
  14. hibernate 中文文档
  15. Arduino--土壤湿度传感器使用(电阻式)
  16. hash与history 以及区别
  17. 小甲鱼第45课 魔术方法 简单定制
  18. 计算机键盘突然不能用了,如果联想笔记本电脑键盘突然无法使用怎么办?
  19. 如何在线重装Win10?Win10电脑系统重装详细教程
  20. 【2023年最新版】渗透测试入门教程,手把手带你进阶渗透测试工程师,学完即可就业

热门文章

  1. Centos7.3编译RAID驱动(一)
  2. SQL Server2008r2审计配置
  3. 实验一Ping 扫描实验
  4. TESTmain.c:(.text+0x15): undefined reference to `TESTscan‘
  5. 51单片机入门教程学习笔记
  6. 华为HCIP-DATACOM题库解析10-20(821)
  7. C#让textbox不能写入
  8. 分享5款轻量级的Win10神器,错过你会后悔的
  9. 九龙证券|超50亿主力资金砸盘计算机行业,这类股获资金青睐
  10. docker login 密码加密保存