前段时间SkeyeLive开放了DirectShow采集库,这个库底层采用DirectShow SDK的接口实现音视频的预览(播放)和采集;很多人可能还不太了解这个封装库的回调方式和之前的DShow线程采集方式有什么不同,或者说对DirectShow的采集流程还不太熟悉,下面我将就Windows平台下用使用DirectShow的过滤器(滤波器)进行流媒体开发的前端采集部分进行简要介绍,如果大家想深入的学习和探索,推荐大家去看看《Visual C 音频/视频处理技术及工程实践》这本书,第9章有详细的流程讲解。
一、枚举采集设备
使用采集设备前,需要首先确定系统已经安装的采集设备:视频、音频采集设备。系统设备枚举器为按类型枚举已注册在系统中的滤波器提供了统一的方法。而且它能够区分不同的硬件设备,即便是同一个滤波器支持它们。这对那些使用Windows驱动模型、KSProxy Filter的设备来说是非常有用的,系统设备枚举器对它们按不同的设备实例进行对待。
当利用系统设备枚举器查询设备的时候,系统设备枚举器为特定类型的设备(如音频捕获和视频压缩)生成了一张枚举表(Enumerator)。类型枚举器(Category Enumerator)为每个这种类型的设备返回一个Moniker,类型枚举器自动把每种即插即用的设备包含在内。
调用标准方法CoCreateInstance生成系统设备枚举器(Device Enumerator),类标识(CLSID)为CLSID_SystemDeviceEnum,方法如下:
代码语言:txt复制CAMERA_LIST_T *CCameraDS::GetCameraList()
{
if (NULL != cameraList.pCamera || cameraList.count > 0)
{
return &cameraList;
}
if (NULL == cameraList.pCamera)
{
cameraList.count = 0;
// enumerate all video capture devices
CComPtr<ICreateDevEnum> pCreateDevEnum;
HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum, (void**)&pCreateDevEnum);
CComPtr<IEnumMoniker> pEm;
hr = pCreateDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEm, 0);
if (hr != NOERROR)
{
return &cameraList;
}
pEm->Reset();
CAMERA_INFO_T *pCameraList = cameraList.pCamera;
CAMERA_INFO_T *pCameraInfo = NULL;
ULONG cFetched;
IMoniker *pM = NULL;
while(hr = pEm->Next(1, &pM, &cFetched), hr==S_OK)
{
pCameraInfo = new CAMERA_INFO_T;
memset(pCameraInfo, 0x00, sizeof(CAMERA_INFO_T));
IPropertyBag *pBag=0;
hr = pM->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pBag);
if(SUCCEEDED(hr))
{
VARIANT var;
var.vt = VT_BSTR;
hr = pBag->Read(L"FriendlyName", &var, NULL); //还有其他属性,像描述信息等等...
if(hr == NOERROR)
{
//获取设备名称
WideCharToMultiByte(CP_ACP,0,var.bstrVal,-1,pCameraInfo->friendlyName, sizeof(pCameraInfo->friendlyName) ,"",NULL);
SysFreeString(var.bstrVal);
}
pBag->Release();
cameraList.count ;
{
pCameraList = cameraList.pCamera;
if (NULL == cameraList.pCamera) cameraList.pCamera = pCameraInfo;
else
{
while (NULL != pCameraList->pNext)
{
pCameraList = pCameraList->pNext;
}
pCameraList->pNext = pCameraInfo;
}
}
}
pM->Release();
}
pCreateDevEnum = NULL;
pEm = NULL;
}
return &cameraList;
}
这是视频设备枚举部分,当然音频也是一样的,只需要把CreateClassEnumerator函数的第一个参数换成CLSID_AudioInputDeviceCategory即可。
注意:调用ICreateDevEnum::CreateClassEnumerator方法生成类型枚举器,参数为用户想要得到的类的ID(CLSID),该方法返回一个IEnumMoniker接口指针。如果指定的类型是空的或不存在,则函数ICreateDevEnum::CreateClassEnumerator将返回S_FALSE而不是错误代码,同时IEnumMoniker指针也是空的,这就要求我们在调用CreateClassEnumerator的时候明确用S_OK进行比较而不使用宏SUCCEEDED;(扯远了...)而在SkeyeLive中还提供了另外一种枚举音频设备的方式,那就是采用DirectSound的枚举方式:DirectSoundCaptureEnumerate这个函数来实现的,需要注意,这个函数枚举出的设备GUID有可能是空的,设备名称可能表象为”声卡主设备驱动“,经测试:这个设备是不能用于采集,也是不存在的,枚举过程中应该丢弃。当然,其实DirectShow也是封装了底层的DirectSound的接口来实现的COM接口的统一封装。
(需要重点说明的是:枚举设备这一块不是DShow封装库中的代码,这是由我们EasyDarwin团队的Gavin大神之前的DShow采集部分代码中提供的(前身是EasyCamera_win),我只是鸠占鹊巢的给大家讲解,向大神致敬~~~哈哈哈!)
二、使用Capture Graph Builder进行音视频采集
这个为了节约篇幅,本文以视频采集为例子进行讲解,其实音频采集是一模一样的(这就是封装的好处,不用关心底层的实现细节);
1、创建GraphBuilder
使用DirectShow进行视频采集,首先,创建视频捕获Graph,DShow SDK提供的是Graph Builder接口是IgraphBuilder。不过针对捕获任务(Capture),还有另一个接口ICaptureGraphBuilder2针对采集捕获的增强型接口,这个接口可以提供视频捕获预览窗口的创建和使用,然后,再创建一个媒体控制器对视频预览的播放进行控制,代码如下:
代码语言:txt复制//创建视频捕获Graph
HRESULT CCaptureVideo::CreateCaptureGraphBuilder()
{
HRESULT hr=NOERROR;
if(m_pGraphBuilder==NULL)
{
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&m_pGraphBuilder);
if(FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder Create m_pGraphBuilder Failed");
return hr;
}
}
if(m_pCaptureGraphBulid==NULL)
{
//// 创建ICaptureGraphBuilder2接口,即创建视频捕获窗
hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC,
IID_ICaptureGraphBuilder2, (void **) &m_pCaptureGraphBulid);
if (FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder CaptureGraphBuilder2 Failed");
return hr;
}
//将捕获窗的视频属性设为定义好的视频窗
//给captureGrap builder指定一个图象filter,不能由于混合视频的呈现,video端口的管理
hr = m_pCaptureGraphBulid->SetFiltergraph(m_pGraphBuilder);
}
//此处可能存在问题是否为一个链路就一个m_pMediaCon媒体控制//QueryInterface
if(m_pMediaCon==NULL)
{
hr = m_pGraphBuilder->QueryInterface(IID_IMediaControl, (void **)&m_pMediaCon);
if (FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder QueryInterface m_pMediaCon Failed");
return hr;
}
}
if(m_pMediaEvent==NULL)
{
hr = m_pGraphBuilder->QueryInterface(IID_IMediaEvent, (void **) &m_pMediaEvent);
if (FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder QueryInterface m_pMediaEvent Failed");
return hr;
}
}
return hr;
}
2、枚举设备并连接设备
枚举设备我们在上一节已经讲过了,这里直接查询到自己需要捕获的设备名称,然后绑定到过滤器上即可:
代码语言:txt复制//枚举设备并绑定设备
BOOL CCaptureVideo::BindToVideoDev(int deviceId, IBaseFilter **pFilter)
{
if (deviceId < 0)
{
return FALSE;
}
CComPtr<ICreateDevEnum> pCreateDevEnum;
HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum, (void**)&pCreateDevEnum);
if (hr != NOERROR)
{
//ERR_DEBUG("Instance DeviceEnum Failed");
return FALSE;
}
CComPtr<IEnumMoniker> pEm;
hr = pCreateDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,&pEm, 0);
if (hr != NOERROR)
{
//ERR_DEBUG("Enum VideoInputDeviceCategory Failed");
return FALSE;
}
pEm->Reset();
ULONG cFetched;
IMoniker *pM=NULL;
int index = 0;
while((( pEm->Next(1, &pM, &cFetched))==S_OK)&&( index <= deviceId))
{
IPropertyBag *pBag=NULL;
if (pM==NULL)
{
break;
}
hr = pM->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pBag);
if(pBag!=NULL)
{
VARIANT var;
var.vt = VT_BSTR;
hr = pBag->Read(L"FriendlyName", &var, NULL);
if (hr == NOERROR)
{
if (index == deviceId)
{
//将视频设备绑定到基础过滤器上
pM->BindToObject(0, 0, IID_IBaseFilter, (void**)pFilter);
}
SysFreeString(var.bstrVal);
pBag->Release();
}
}
pM->Release();
index ;
}
return TRUE;
}
3、采集参数的设置
采集前需要对要采集的视频格式、图像质量进行设置,如视频的分辨率、帧率和数据格式,图像的亮度、色度和饱和度参数设置等。当然,我们这里只针对视频的宽高,帧率和数据格式进行了设置,如果大家还想进行更多的设置,可以使用OleCreatePropertyFrame函数以属性页的方式对视频属性和图像参数进行配置和修改。
代码语言:txt复制HRESULT CCaptureVideo::SetVideoSize(int nPreview,CString strRGBBytes,int nFrameRate,int iWidth , int iHeight)
{
HRESULT hr=E_FAIL;
if(m_pCaptureGraphBulid==NULL)
return hr;
IAMStreamConfig *pAMStreamConfig=NULL;
if(nPreview==0)
{
hr = m_pCaptureGraphBulid->FindInterface(&PIN_CATEGORY_PREVIEW,&MEDIATYPE_Video,
m_pBaseFilter,IID_IAMStreamConfig,(void **)&pAMStreamConfig);
}
else
{
hr = m_pCaptureGraphBulid->FindInterface(&PIN_CATEGORY_CAPTURE,&MEDIATYPE_Video,
m_pBaseFilter,IID_IAMStreamConfig,(void **)&pAMStreamConfig);
}
if(FAILED( hr ))
{
SAFE_RELEASE(pAMStreamConfig);
return hr;
}
//得到视频格式大小
AM_MEDIA_TYPE *pmt;
pAMStreamConfig->GetFormat(&pmt);
//设置视频格式
pmt->majortype = MEDIATYPE_Video;
pmt->subtype = GetMediaTypeGuid(strRGBBytes);
VIDEOINFOHEADER *pvih = reinterpret_cast<VIDEOINFOHEADER *>(pmt->pbFormat);
//设置回去
int nDefualWidth = pvih->bmiHeader.biWidth;
int nDefualHeight = pvih->bmiHeader.biHeight;
pvih->bmiHeader.biWidth = iWidth;
pvih->bmiHeader.biHeight = iHeight;
pvih->bmiHeader.biSizeImage = pmt->lSampleSize = iWidth*iHeight*pvih->bmiHeader.biPlanes*pvih->bmiHeader.biBitCount/8;
pvih->AvgTimePerFrame = (LONGLONG)(10000000/nFrameRate);
hr = pAMStreamConfig->SetFormat(pmt);
if(FAILED(hr))
{
//如果设置失败可以选用默认的,但运用之后,小屏幕初始化时会出现闪动的情况
pvih->bmiHeader.biWidth = nDefualWidth;
pvih->bmiHeader.biHeight = nDefualHeight;
pvih->bmiHeader.biSizeImage = pmt->lSampleSize = nDefualWidth*nDefualHeight*pvih->bmiHeader.biPlanes*pvih->bmiHeader.biBitCount/8;
hr = pAMStreamConfig->SetFormat(pmt);
if(FAILED(hr))
{
SAFE_RELEASE(pAMStreamConfig);
FreeMediaType(*pmt);
//ERR_DEBUG("初始化设置视频格式失败");
return hr;
}
}
SAFE_RELEASE(pAMStreamConfig);
FreeMediaType(*pmt);//
return hr;
}
4、建立视频渲染器
创建视频渲染器(呈现器),对捕获视频进行显示,代码如下:
代码语言:txt复制HRESULT CCaptureVideo::CreateVideoRender(int nType)
{
HRESULT hr = NOERROR;
if(m_pWindowRender)
{
SAFE_RELEASE(m_pWindowRender);
}
//创建解码器filter,CLSID_VideoRendererDefault,//
if(nType==0)
{
SAFE_RELEASE(m_pWindowRender);
return NOERROR;
}
else if(nType==1)
{
hr = CoCreateInstance(CLSID_VideoRendererDefault,0,CLSCTX_INPROC_SERVER,
IID_IBaseFilter,(void **)&m_pWindowRender);
}
else if(nType==2)
{
hr = CoCreateInstance(CLSID_VideoRenderer,0,CLSCTX_INPROC_SERVER,
IID_IBaseFilter,(void **)&m_pWindowRender);
}
else if(nType==3)//不显示
{
hr = CoCreateInstance(CLSID_NullRenderer,0,CLSCTX_INPROC_SERVER,
IID_IBaseFilter,(void **)&m_pWindowRender);
}
else
{
SAFE_RELEASE(m_pWindowRender);
return NOERROR;
}
if(FAILED(hr))
{
SAFE_RELEASE(m_pWindowRender);
//ERR_DEBUG("接收创建呈现器失败");
return hr;
}
if(m_pGraphBuilder)
{
hr = m_pGraphBuilder->AddFilter(m_pWindowRender,L"recv render");
if(FAILED(hr))
{
SAFE_RELEASE(m_pWindowRender);
//ERR_DEBUG("加入接收创建呈现器失败");
return hr;
}
}
return hr;
}
5、预览采集到的视频数据
首先,初始化过滤器链路管理器,把指定采集设备的过滤器添加到链路中,然后渲染RenderStream方法把所有的过滤器链接起来,最后根据设定的显示窗口预览采集到的视频数据,具体实现过程如下:
代码语言:txt复制 hr = CreateCaptureSampleGrabber(m_strRGBByte);
if(FAILED(hr))
{
SAFE_RELEASE(m_pSampleGrabberFilter);
SAFE_RELEASE(m_pSampleGrabber);
//ERR_DEBUG("CreateCaptureSampleGrabber Failed");
return -1;
}
if(m_nDeinterlace==1)//m_iHeight/m_iWidth!=(1.5/4))
{
CreateDeinterlaceFilter();
}
if(m_pVideoDeinterlaceFilter)
{
hr = m_pCaptureGraphBulid->RenderStream(&pCategorySuc,&MEDIATYPE_Video,m_pBaseFilter,m_pVideoDeinterlaceFilter,m_pSampleGrabberFilter);
hr = m_pCaptureGraphBulid->RenderStream(NULL,NULL,m_pSampleGrabberFilter,NULL,m_pWindowRender);
if(FAILED(hr))
{
hr = m_pCaptureGraphBulid->RenderStream(&PIN_CATEGORY_PREVIEW,&MEDIATYPE_Video,m_pBaseFilter,m_pSampleGrabberFilter,NULL);
if(FAILED(hr))
{
//ERR_DEBUG("PrivewVideoDev RenderStream Failed ");
return -1;
}
}
}
else
{
hr = m_pCaptureGraphBulid->RenderStream(&pCategorySuc,&MEDIATYPE_Video,m_pBaseFilter,m_pSampleGrabberFilter,m_pWindowRender);
if(FAILED(hr))
{
hr = m_pCaptureGraphBulid->RenderStream(&pCategoryFail,&MEDIATYPE_Video,m_pBaseFilter,m_pSampleGrabberFilter,m_pWindowRender);
if(FAILED(hr))
{
//ERR_DEBUG("PrivewVideoDev RenderStream Failed ");
return -1;
}
}
}
if(m_bThread)
{
m_pSampleGrabber->SetOneShot(FALSE);
m_pSampleGrabber->SetBufferSamples(TRUE);
// m_pSampleGrabber->SetOneShot(TRUE);
}
else
{
m_pSampleGrabber->SetBufferSamples(TRUE);
m_pSampleGrabber->SetOneShot(FALSE);
//m_nDataType:数据类型1--音频,2--视频数据
//nIndex:设备编号:音频-1,视频0---N
m_cSampleGrabberCB.SetDataInfo(m_nIndex, m_nDataType);
int nMode=1;//0--SampleCB,1--BufferCB
m_pSampleGrabber->SetCallback(&m_cSampleGrabberCB, nMode);
}
if(m_pVideoWin==NULL&&m_nRenderType!=2)
{
hr = m_pCaptureGraphBulid->FindInterface(&pCategorySuc,&MEDIATYPE_Video,//CAPTURE
m_pBaseFilter,IID_IVideoWindow,(void **)&m_pVideoWin);
if (FAILED(hr))//建立失败则查找CAPTURE口
{
hr = m_pCaptureGraphBulid->FindInterface(&pCategoryFail,&MEDIATYPE_Video,//CAPTURE
m_pBaseFilter,IID_IVideoWindow,(void **)&m_pVideoWin);
if (FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder QueryInterface m_pVideoWin Failed");
return -1;
}
}
}
//设置视频显示窗口
SetupVideoWindow();
hr = StartPreview();
if(FAILED(hr))
{
return -1;
}
然后,设置视频显示窗口:
代码语言:txt复制//设置窗口句柄,并自动更新显示大小
HRESULT CCaptureVideo::SetupVideoWindow(HWND hWnd)
{
HRESULT hr=NOERROR;
if(m_pVideoWin==NULL)
{
return hr;
}
if(hWnd!=NULL)
m_hWnd = hWnd;
hr = m_pVideoWin->put_Owner((OAHWND)m_hWnd);
// hr = m_pVideoWin->put_MessageDrain((OAHWND)m_hWnd);
if (FAILED(hr))
{
//ERR_DEBUG("SetupVideoWindow put_Owner Error");
return hr;
}
hr = m_pVideoWin->put_WindowStyle(WS_CHILD |SS_NOTIFY| WS_CLIPCHILDREN);
if (FAILED(hr))
{
//ERR_DEBUG("SetupVideoWindow put_WindowStyle");
return hr;
}
ResizeVideoWindow();
hr = m_pVideoWin->put_Visible(OATRUE);
return hr;
}
最后,开始预览:
代码语言:txt复制HRESULT CCaptureVideo::StartPreview()
{
HRESULT hr=NOERROR;
if(m_pMediaCon==NULL)
{
hr = m_pGraphBuilder->QueryInterface(IID_IMediaControl, (void **)&m_pMediaCon);
if(SUCCEEDED(hr))
{
hr = m_pMediaCon->Run();
if(FAILED(hr))
{
m_pMediaCon->Stop();
}
}
}
else
{
hr = m_pMediaCon->Run();
if(FAILED(hr))
{
m_pMediaCon->Stop();
}
}
return hr;
}
至此,DShow对视频的采集整个完整的流程就完成了。
三、DShow采集的两种模式
1、线程模式(拉模式)
线程模式采用多线程的方式,在线程回调中调用GetCurrentBuffer函数获取采集缓存中的一帧数据,这里获取的数据是之前设置的色彩格式的数据(如果设置成功的话,否则是默认格式);如果要得到指定的格式需要进行色彩格式转换。
线程执行函数如下:
代码语言:txt复制void CCaptureVideo::OnThreadDeal()
{
BYTE *pData=NULL;
long lDatasize=0;
char strMediaType[24]=_T("YUY2");
if(!m_strRGBByte.IsEmpty())
{
strcpy_s(strMediaType, 24, m_strRGBByte);
}
//读取缓冲区数据
pData=GetCurrentBuffer(lDatasize,strMediaType);
//TRACE("OnThreadDeal:%drn",lDatasize);
if (m_sThreadCalbackInfo.realDataCalFunc&&m_sThreadCalbackInfo.pMaster&&pData&&lDatasize>0)
{
//执行数据回调
m_sThreadCalbackInfo.realDataCalFunc(m_nIndex, pData, lDatasize,
(RealDataStreamType)m_nDataType,NULL, m_sThreadCalbackInfo.pMaster);
}
}
2、回调模式(推模式)
回调模式是利用ISampleGrabber提供的回调函数接口进行设置,该设置回调函数原型如下:
代码语言:txt复制 virtual HRESULT STDMETHODCALLTYPE ISampleGrabber::SetCallback(
ISampleGrabberCB *pCallback,
long WhichMethodToCallback) = 0;
从函数接口我们可以看出,第一个参数需要一个ISampleGrabberCB类型的参数,我们自定义一个回调接口类:class CSampleGrabberCB:public ISampleGrabberCB,用以处理回调数据的接收及外调;
设置接口:
代码语言:txt复制/*
nDataType:数据类型1--音频,2--视频数据
nIndex:设备编号:音频-1,视频0---N
*/
void CSampleGrabberCB::SetDataInfo(int nIndex,int nDataType)
{
m_nIndex=nIndex;
m_nDataType= nDataType;
}
回调函数处理:
代码语言:txt复制//统一的回调函数
STDMETHODIMP CSampleGrabberCB::BufferCB( double dblSampleTime, BYTE *pBuffer, long lBufferSize )
{
if (!pBuffer)
return E_POINTER;
now_tick=::GetTickCount();
first_tick = now_tick;
if (m_nDataType == 1)//视频
{
// TRACE("BufferCB:%d-%d-%d-%drn", m_nIndex, m_nDataType, now_tick - first_tick, lBufferSize);
}
else if (m_nDataType == 2)//音频
{
// TRACE("BufferCB:%d-%d-%d-%drn", m_nIndex, m_nDataType, now_tick - first_tick, lBufferSize);
}
if(pBuffer==NULL||lBufferSize<=0)
return 0;
if (m_realDataCallback && m_pMaster)
{
m_realDataCallback(m_nIndex, pBuffer, lBufferSize, (RealDataStreamType)m_nDataType,NULL, m_pMaster);
return 0;
}
if( g_cbInfo.lBufferSize < lBufferSize )
{
delete [] g_cbInfo.pBuffer;
g_cbInfo.pBuffer = NULL;
g_cbInfo.lBufferSize = 0;
g_cbInfo.bHaveData=FALSE;
}
// Since we can 't access Windows API functions in this callback, just
// copy the bitmap data to a global structure for later reference.
g_cbInfo.dblSampleTime = dblSampleTime;
// If we haven 't yet allocated the data buffer, do it now.
// Just allocate what we need to store the new bitmap.
if (!g_cbInfo.pBuffer)
{
g_cbInfo.pBuffer = new BYTE[lBufferSize];
g_cbInfo.lBufferSize = lBufferSize;
}
if( !g_cbInfo.pBuffer )
{
g_cbInfo.lBufferSize = 0;
g_cbInfo.bHaveData=FALSE;
return E_OUTOFMEMORY;
}
// Copy the bitmap data into our global buffer
memcpy(g_cbInfo.pBuffer, pBuffer, lBufferSize);
g_cbInfo.bHaveData=TRUE;
return 0;
}
两种方式相比较,个人观点:各有优劣;在SkeyeLive中我们采用的是回调方式,当时引进这个库就是为了能在采集端保证音视频从源头是同步的,当然,其实线程模式也是能实现同步的;线程模式的优点是:采集即时性高,即需即取,几乎不会有延时,缺点就是:如果出现取数据端不及时时,如果不考虑缓存的情况下可能就会出现丢帧。而回调模式就正好相反,其优点是:稳定性高,随时都能保证取的帧是连续的,即使不做缓存也不会出现取出来的数据出现丢帧的情况,当然在取数据时比如编码慢(或者回调中做其他延时处理),就会出现预览和回调同步延时的情况,回调缓存的数据量会越来越大,延时也将增大;当然,如果在多路同时采集时,甚至多路同时进行数据处理时,采用回调模式会更显优势!