• 实现 DirectShow 虚拟 Camera 驱动


    今天我们要实现一个虚拟 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 有三个方法:

    MethodDescription
    GetRetrieves a property identified by a property set GUID and a property ID.
    QuerySupportedDetermines whether an object supports a specified property set.
    SetSets a property identified by a property set GUID and a property ID.

     不支持任何设置操作:

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

    只支持获取 catagory 属性:

    1. // Get: Return the pin category (our only property).
    2. HRESULT CMyCapturePin::Get(
    3. REFGUID guidPropSet, // Which property set.
    4. DWORD dwPropID, // Which property in that set.
    5. void *pInstanceData, // Instance data (ignore).
    6. DWORD cbInstanceData, // Size of the instance data (ignore).
    7. void *pPropData, // Buffer to receive the property data.
    8. DWORD cbPropData, // Size of the buffer.
    9. DWORD *pcbReturned // Return the size of the property.
    10. )
    11. {
    12. if (guidPropSet != AMPROPSETID_Pin)
    13. return E_PROP_SET_UNSUPPORTED;
    14. if (dwPropID != AMPROPERTY_PIN_CATEGORY)
    15. return E_PROP_ID_UNSUPPORTED;
    16. if (pPropData == NULL && pcbReturned == NULL)
    17. return E_POINTER;
    18. if (pcbReturned)
    19. *pcbReturned = sizeof(GUID);
    20. if (pPropData == NULL) // Caller just wants to know the size.
    21. return S_OK;
    22. if (cbPropData < sizeof(GUID)) // The buffer is too small.
    23. return E_UNEXPECTED;
    24. *(GUID *)pPropData = PIN_CATEGORY_CAPTURE;
    25. return S_OK;
    26. }

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

    1. // QuerySupported: Query whether the pin supports the specified property.
    2. HRESULT CMyCapturePin::QuerySupported(REFGUID guidPropSet, DWORD dwPropID,
    3. DWORD *pTypeSupport)
    4. {
    5. if (guidPropSet != AMPROPSETID_Pin)
    6. return E_PROP_SET_UNSUPPORTED;
    7. if (dwPropID != AMPROPERTY_PIN_CATEGORY)
    8. return E_PROP_ID_UNSUPPORTED;
    9. if (pTypeSupport)
    10. // We support getting this property, but not setting it.
    11. *pTypeSupport = KSPROPERTY_SUPPORT_GET;
    12. return S_OK;
    13. }

    实现 Capture 的注册

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

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

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

    1. IFilterMapper2* fm = 0;
    2. hr = CreateComObject(CLSID_FilterMapper2, IID_IFilterMapper2, fm);
    3. if (SUCCEEDED(hr))
    4. {
    5. if (bRegister)
    6. {
    7. IMoniker* pMoniker = 0;
    8. REGFILTER2 rf2;
    9. rf2.dwVersion = 1;
    10. rf2.dwMerit = MERIT_DO_NOT_USE;
    11. rf2.cPins = 1;
    12. rf2.rgPins = sudMyPin;
    13. // this is the name that actually shows up in VLC et al. weird
    14. hr = fm->RegisterFilter(CLSID_MyCamera, g_wszMyCamera, &pMoniker, &CLSID_VideoInputDeviceCategory, NULL, &rf2);
    15. pMoniker->Release();
    16. }
    17. else
    18. {
    19. hr = fm->UnregisterFilter(&CLSID_VideoInputDeviceCategory, 0, CLSID_MyCamera);
    20. }
    21. }
    22. // release interface
    23. //
    24. if (fm)
    25. 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.
    1. HRESULT STDMETHODCALLTYPE CMyCapturePin::GetNumberOfCapabilities(int* piCount, int* piSize)
    2. {
    3. *piCount = 1;
    4. *piSize = sizeof(VIDEO_STREAM_CONFIG_CAPS); // VIDEO_STREAM_CONFIG_CAPS is an MS struct
    5. return S_OK;
    6. }

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

    1. HRESULT STDMETHODCALLTYPE CMyCapturePin::GetStreamCaps(int iIndex, AM_MEDIA_TYPE** pmt, BYTE* pSCC)
    2. {
    3. CAutoLock cAutoLock(m_pFilter->pStateLock());
    4. HRESULT hr = GetMediaType(&m_mt); // setup then re-use m_mt ... why not?
    5. // some are indeed shared, apparently.
    6. if (FAILED(hr))
    7. {
    8. return hr;
    9. }
    10. *pmt = CreateMediaType(&m_mt); // a windows lib method, also does a copy for us
    11. if (*pmt == NULL) return E_OUTOFMEMORY;
    12. DECLARE_PTR(VIDEO_STREAM_CONFIG_CAPS, pvscc, pSCC);
    13. /*
    14. most of these are listed as deprecated by msdn... yet some still used, apparently. odd.
    15. */
    16. pvscc->VideoStandard = AnalogVideo_None;
    17. pvscc->InputSize.cx = m_info->format.video.width;
    18. pvscc->InputSize.cy = m_info->format.video.height;
    19. // most of these values are fakes..
    20. pvscc->MinCroppingSize.cx = m_info->format.video.width;
    21. pvscc->MinCroppingSize.cy = m_info->format.video.height;
    22. pvscc->MaxCroppingSize.cx = m_info->format.video.width;
    23. pvscc->MaxCroppingSize.cy = m_info->format.video.height;
    24. pvscc->CropGranularityX = 1;
    25. pvscc->CropGranularityY = 1;
    26. pvscc->CropAlignX = 1;
    27. pvscc->CropAlignY = 1;
    28. pvscc->MinOutputSize.cx = m_info->format.video.width;
    29. pvscc->MinOutputSize.cy = m_info->format.video.height;
    30. pvscc->MaxOutputSize.cx = m_info->format.video.width;
    31. pvscc->MaxOutputSize.cy = m_info->format.video.height;
    32. pvscc->OutputGranularityX = 1;
    33. pvscc->OutputGranularityY = 1;
    34. pvscc->StretchTapsX = 1; // We do 1 tap. I guess...
    35. pvscc->StretchTapsY = 1;
    36. pvscc->ShrinkTapsX = 1;
    37. pvscc->ShrinkTapsY = 1;
    38. pvscc->MinFrameInterval = 500000; // the larger default is actually the MinFrameInterval, not the max
    39. pvscc->MaxFrameInterval = 500000000; // 0.02 fps :) [though it could go lower, really...]
    40. pvscc->MinBitsPerSecond = (LONG)1 * 1 * 8 * m_info->format.video.frame_rate; // if in 8 bit mode 1x1. I guess.
    41. 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?
    42. return hr;
    43. }

     外部获取当前媒体格式:

    1. HRESULT STDMETHODCALLTYPE CMyCapturePin::GetFormat(AM_MEDIA_TYPE** ppmt)
    2. {
    3. CAutoLock cAutoLock(m_pFilter->pStateLock());
    4. if (!m_bFormatAlreadySet) {
    5. HRESULT hr = GetMediaType(&m_mt); // setup with index "0" kind of the default/preferred...I guess...
    6. if (FAILED(hr))
    7. {
    8. return hr;
    9. }
    10. }
    11. *ppmt = CreateMediaType(&m_mt); // windows internal method, also does copy
    12. return S_OK;
    13. }

    外部配置媒体格式:

    1. HRESULT STDMETHODCALLTYPE CMyCapturePin::SetFormat(AM_MEDIA_TYPE* pmt)
    2. {
    3. CAutoLock cAutoLock(m_pFilter->pStateLock());
    4. // I *think* it can go back and forth, then. You can call GetStreamCaps to enumerate, then call
    5. // SetFormat, then later calls to GetMediaType/GetStreamCaps/EnumMediatypes will all "have" to just give this one
    6. // though theoretically they could also call EnumMediaTypes, then Set MediaType, and not call SetFormat
    7. // does flash call both? what order for flash/ffmpeg/vlc calling both?
    8. // LODO update msdn
    9. // "they" [can] call this...see msdn for SetFormat
    10. // NULL means reset to default type...
    11. if (pmt != NULL)
    12. {
    13. if (pmt->formattype != FORMAT_VideoInfo) // FORMAT_VideoInfo == {CLSID_KsDataTypeHandlerVideo}
    14. return E_FAIL;
    15. // LODO I should do more here...http://msdn.microsoft.com/en-us/library/dd319788.aspx I guess [meh]
    16. // LODO should fail if we're already streaming... [?]
    17. if (CheckMediaType((CMediaType*)pmt) != S_OK) {
    18. return E_FAIL; // just in case :P [FME...]
    19. }
    20. VIDEOINFOHEADER* pvi = (VIDEOINFOHEADER*)pmt->pbFormat;
    21. // for FMLE's benefit, only accept a setFormat of our "final" width [force setting via registry I guess, otherwise it only shows 80x60 whoa!]
    22. // flash media live encoder uses setFormat to determine widths [?] and then only displays the smallest? huh?
    23. if (pvi->bmiHeader.biWidth != m_info->format.video.width ||
    24. pvi->bmiHeader.biHeight != m_info->format.video.height)
    25. {
    26. return E_INVALIDARG;
    27. }
    28. // ignore other things like cropping requests for now...
    29. // 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 guess
    30. m_mt = *pmt;
    31. }
    32. IPin* pin;
    33. ConnectedTo(&pin);
    34. if (pin)
    35. {
    36. IFilterGraph* pGraph = m_pFilter->GetFilterGraph();
    37. HRESULT res = pGraph->Reconnect(this);
    38. if (res != S_OK) // LODO check first, and then just re-use the old one?
    39. 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...
    40. // plus ours is a weird case...
    41. }
    42. else {
    43. // graph hasn't been built yet...
    44. // so we're ok with "whatever" format they pass us, we're just in the setup phase...
    45. }
    46. // success of some type
    47. if (pmt == NULL) {
    48. m_bFormatAlreadySet = FALSE;
    49. }
    50. else {
    51. m_bFormatAlreadySet = TRUE;
    52. }
    53. return S_OK;
    54. }

     

    实现 IPropertyPage

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

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

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

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

    1. virtual HRESULT OnConnect(IUnknown* pUnk);
    2. virtual HRESULT OnActivate();
    3. virtual INT_PTR OnReceiveMessage(HWND hwnd,
    4. UINT uMsg, WPARAM wParam, LPARAM lParam);
    5. virtual HRESULT OnApplyChanges();
    6. virtual HRESULT OnDisconnect();

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

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

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

    实现该接口:

    1. STDMETHODIMP_(HRESULT __stdcall) CMyCamera::GetUrl(LPCTSTR* psUrl)
    2. {
    3. *psUrl = m_URL;
    4. return S_OK;
    5. }
    6. STDMETHODIMP_(HRESULT __stdcall) CMyCamera::SetUrl(LPCTSTR sUrl)
    7. {
    8. lstrcpyW(m_URL, sUrl);
    9. Load(m_URL, NULL);
    10. return S_OK;
    11. }

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

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

    1. HRESULT CMyPropertyPage::OnConnect(IUnknown* pUnk)
    2. {
    3. if (pUnk == NULL)
    4. {
    5. return E_POINTER;
    6. }
    7. ASSERT(m_pConfig == NULL);
    8. return pUnk->QueryInterface(IID_ICameraConfig,
    9. reinterpret_cast<void**>(&m_pConfig));
    10. }

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

    1. HRESULT CMyPropertyPage::OnActivate()
    2. {
    3. ASSERT(m_pConfig != NULL);
    4. LPCTSTR url;
    5. HRESULT hr = m_pConfig->GetUrl(&url);
    6. if (SUCCEEDED(hr))
    7. {
    8. SendDlgItemMessage(m_Dlg, IDC_URL, WM_SETTEXT, 0, (LPARAM)url);
    9. }
    10. return hr;
    11. }

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

    1. INT_PTR CMyPropertyPage::OnReceiveMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
    2. {
    3. switch (uMsg)
    4. {
    5. case WM_COMMAND:
    6. if (wParam == MAKEWPARAM(IDC_URL, EN_CHANGE)) {
    7. if (IsWindowVisible(m_hwnd))
    8. SetDirty();
    9. }
    10. break;
    11. } // Switch.
    12. // Let the parent class handle the message.
    13. return CBasePropertyPage::OnReceiveMessage(hwnd, uMsg, wParam, lParam);
    14. }
    15. void CMyPropertyPage::SetDirty()
    16. {
    17. m_bDirty = TRUE;
    18. if (m_pPageSite)
    19. {
    20. m_pPageSite->OnStatusChange(PROPPAGESTATUS_DIRTY);
    21. }
    22. }

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

    1. HRESULT CMyPropertyPage::OnApplyChanges() {
    2. ASSERT(m_pConfig != NULL);
    3. TCHAR url[MAX_PATH];
    4. SendDlgItemMessage(m_Dlg, IDC_URL, WM_GETTEXT, MAX_PATH, (LPARAM)url);
    5. HRESULT hr = m_pConfig->SetUrl(url);
    6. return hr;
    7. }

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

    1. HRESULT CMyPropertyPage::OnDisconnect()
    2. {
    3. if (m_pConfig)
    4. {
    5. m_pConfig->Release();
    6. m_pConfig = NULL;
    7. }
    8. return S_OK;
    9. }

    实现 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:

    1. CFactoryTemplate g_Templates[] =
    2. {
    3. ......,
    4. {
    5. L"My Camera Property Page",
    6. & CLSID_MyCameraPropertyPage,
    7. CMyPropertyPage::CreateInstance,
    8. NULL,
    9. NULL
    10. }
    11. };

    实现 GetPages 方法: 

    1. STDMETHODIMP_(HRESULT __stdcall) CMyCamera::GetPages(CAUUID* pPages)
    2. {
    3. pPages->cElems = 1;
    4. pPages->pElems = (GUID*)CoTaskMemAlloc(sizeof(GUID));
    5. if (pPages->pElems == NULL)
    6. {
    7. return E_OUTOFMEMORY;
    8. }
    9. pPages->pElems[0] = CLSID_MyCameraPropertyPage;
    10. return S_OK;
    11. }

  • 相关阅读:
    Python GUI编程之PyQt5入门到实战
    写年度总结报告的注意事项
    echarts3 map
    Maven的使用
    基于SpringBoot的网上超市系统的设计与实现
    TCP/IP Illustrated Episode 7
    【数据结构】深入了解队列
    vue中babel-plugin-component按需引入和element-ui 的主题定制,支持发布,上线
    〖Python 数据库开发实战 - Redis篇②〗- Linux系统下安装 Redis 数据库
    使用VSCode中遇到的问题及解决办法
  • 原文地址:https://blog.csdn.net/luansxx/article/details/126795567