使用不同的 ISampleGrabber 模式从流式 URL 捕获图像






4.57/5 (19投票s)
本文介绍了如何使用不同的 ISampleGrabber 模式从流 URL 捕获图像。
引言
本文解释了如何从流 URL(例如 WME 的流)捕获帧并使用 DirectShow 将其保存为 BMP。在本文中,我使用了 WMASF Reader(网络读取器)的 IBase
过滤器来读取网络资源,并使用 ISampleGrabber
过滤器捕获位图帧。在这里,我尝试解释使用 ISampleGrabber
过滤器捕获位图的两种方法(SetCallBack
和 GetCurrentBuffer
)。
背景
在我最近的一个项目中,我遇到了同样的问题,即从 WME 流 URL 捕获快照。我在网上搜索了一下,看到了很多关于从电影文件或其他内容捕获图像的文章,但没有关于如何从流 URL 捕获图像的文章。我借助 MSDN 和网络上的一些参考资料实现了这一点。最初,我通过使用 IFileSourceFilter
和 ISampleGrabber
接口中的 GetCurrentBuffer
方法来实现了一个解决方案。这对我来说已经足够了。之后,我尝试了 ISampleGrabber
接口中的 CallBack
(SetCallBack
) 以获得不同的体验。我在文中解释了这两种模式。我将尝试解释我学到和实现的东西。我希望示例代码能帮助解决您的问题。
设置您的 Visual Studio 项目
您需要将 Windows Platform SDK 和 DShow 基类的头文件添加到您的包含路径中。项目必须链接 Strmbase.lib。
#include "qedit.h" // SampleGrabber filter
#include "atlbase.h" // for using atl support
#include "dshow.h" // DirectShow header
Using the Code
本文将使用以下 DShow 接口...
IGraphBuilder *pGraph = NULL; //Graph Manager
IMediaControl *pControl = NULL; //Media Control for run the graph
IMediaEvent *pEvent = NULL; //Media Event interface for capture the media events
IBaseFilter *pWMASFReader = NULL; //IBase Filter for WM ASF READER (network reader)
IPin *pStreamOut = NULL,*pStreamRender = NULL; //IPin interfaces for connecting filters
IFileSourceFilter *pIFileSourceFilter = NULL; //for specifiying network target....
首先,初始化 COM 并创建过滤器图管理器。
HRESULT hr = CoInitialize(NULL);
if(FAILED(hr))
return FALSE ;
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&pGraph);
if(FAILED(hr))
return FALSE;
创建用于网络读取的 WM ASF Reader 过滤器,并将其添加到图中。然后,查询文件源过滤器以指定网络位置,并使用 IFileSourceFilter::Load()
方法加载指定的网络位置(例如:http://host:port)。它将加载所需的网络源;如果找不到网络源或网络源无效,则会返回带有结果代码的 HRESULT
值。m_strURL
是一个 CString
变量,其中包含所需网络位置。
hr = CoCreateInstance(CLSID_WMAsfReader,NULL,CLSCTX_INPROC_SERVER,
IID_IBaseFilter, (void **) &pWMASFReader);
if (SUCCEEDED(hr))
{
hr = pGraph->AddFilter(pWMASFReader,L"WM ASF Reader");
if (SUCCEEDED(hr))
{
hr = pWMASFReader->QueryInterface(IID_IFileSourceFilter,
(void **) &pIFileSourceFilter);
if (SUCCEEDED(hr))
{
// load network source
hr = pIFileSourceFilter->Load(m_strURL.AllocSysString(), NULL);
}
}
}
if(FAILED(hr))
return FALSE;
现在我们已经有了网络源。我们需要创建 Sample Grabber 过滤器并获取 Sample Grabber 接口。创建 Sample Grabber 过滤器,将其添加到图中,并查询 ISampleGrabber
(ISampleGrabber *m_pGrabber
)。
// Create the Sample Grabber.
IBaseFilter *pGrabberF = NULL;
hr = CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER,
IID_IBaseFilter, (void**)&pGrabberF);
if (FAILED(hr))
{
// Return an error.
return FALSE;
}
hr = pGraph->AddFilter(pGrabberF, L"Sample Grabber");
if (FAILED(hr))
{
// Return an error.
return FALSE;
}
pGrabberF->QueryInterface(IID_ISampleGrabber, (void**)&m_pGrabber);
我们需要为 Sample Grabber 输入引脚上的连接指定媒体类型。为此,我们将使用 AM_MEDIA_TYPE
结构。
AM_MEDIA_TYPE mt;
ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
mt.majortype = MEDIATYPE_Video;
mt.subtype = MEDIASUBTYPE_RGB24;
hr = m_pGrabber->SetMediaType(&mt);
如果需要,您可以向图中添加一个 null renderer 过滤器,以防止显示预览窗口。null renderer 过滤器是一个丢弃它接收到的每个样本的渲染器,而不显示或渲染样本数据。否则,您可以从图中查询 IVideoWindow
并处理弹出窗口。
IBaseFilter *pNullRenderer;
hr = CoCreateInstance (CLSID_NullRenderer, NULL, CLSCTX_INPROC_SERVER,
IID_IBaseFilter, (void **)&pNullRenderer);
hr = pGraph->AddFilter(pNullRenderer, L"Null Renderer");
现在我们将把网络读取器过滤器连接到 grabber 过滤器。Sample Grabber 是一个转换过滤器,因此输出引脚必须连接到另一个过滤器。通常,您可能只想在完成样本后丢弃它们。在这种情况下,将 Sample Grabber 连接到 null renderer 过滤器,后者会丢弃它接收到的数据。
hr = ConnectFilters(pGraph, pWMASFReader, pGrabberF);
以下函数将第一个过滤器连接到第二个过滤器
HRESULT CVideoImageCapDlg::ConnectFilters(IGraphBuilder *pGraph,
IBaseFilter *pFirst, IBaseFilter *pSecond)
{
IPin *pOut = NULL, *pIn = NULL;
HRESULT hr = GetPin(pSecond, PINDIR_INPUT, &pIn);
if (FAILED(hr)) return hr;
// The previous filter may have multiple outputs, so try each one!
IEnumPins *pEnum;
pFirst->EnumPins(&pEnum);
while(pEnum->Next(1, &pOut, 0) == S_OK)
{
PIN_DIRECTION PinDirThis;
pOut->QueryDirection(&PinDirThis);
if (PINDIR_OUTPUT == PinDirThis)
{
hr = pGraph->Connect(pOut, pIn);
if(!FAILED(hr))
{
break;
}
}
pOut->Release();
}
pEnum->Release();
pIn->Release();
pOut->Release();
return hr;
}
然后,获取输出引脚并渲染流。
IPin * pGrabOutPin=NULL;
hr= GetPin( pGrabberF,PINDIR_OUTPUT,&pGrabOutPin);
HRESULT CVideoImageCapDlg::GetPin(IBaseFilter *pFilter, PIN_DIRECTION PinDir, IPin **ppPin)
{
IEnumPins *pEnum;
IPin *pPin;
pFilter->EnumPins(&pEnum);
while(pEnum->Next(1, &pPin, 0) == S_OK)
{
PIN_DIRECTION PinDirThis;
pPin->QueryDirection(&PinDirThis);
if (PinDir == PinDirThis)
{
pEnum->Release();
*ppPin = pPin;
return S_OK;
}
pPin->Release();
}
pEnum->Release();
return E_FAIL;
}
hr = pGraph->Render(pGrabOutPin);
if( FAILED( hr ) )
{
AfxMessageBox("Could not render grabber output pin\r\n");
return FALSE;
}
现在查询 Media control 以运行图,并查询 Media event 以检索事件通知。
hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);
if(FAILED(hr))
return FALSE;
hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
if(FAILED(hr))
return FALSE ;
Sample Grabber 以两种模式之一运行
- 缓冲模式在将样本传递到下游之前复制每个样本。
- 回调模式在每个样本上调用应用程序定义的函数。
调用 ISampleGrabber::SetOneShot
方法并将值设置为 FALSE
。这会使 Sample Grabber 在接收到第一个媒体样本后继续运行图。然后,调用 ISampleGrabber::SetBufferSamples
并将值设置为 TRUE
,以指定是否在样本数据通过过滤器时将其复制到缓冲区。这样,您就可以通过调用 ISampleGrabber::GetCurrentBuffer
来获取复制的缓冲区。
// Set one-shot mode false and buffering.
hr = m_pGrabber->SetOneShot(FALSE);
hr = m_pGrabber->SetBufferSamples(TRUE);
如果一切顺利,我们就可以运行图了。
long evCode=0;
hr=pControl->Run(); // Run the graph.
hr=pEvent->WaitForCompletion(INFINITE, &evCode); // Wait till its done.
//please be carefull when using INFINITE,
//this may block the entire app indefinitely.
//It will cause the application hang up.
Sleep(1000);
如果图正常运行,我们就可以使用 ISampleGrabber::GetCurrentBuffer()
方法检索当前缓冲区。SaveBufferToBitmap(mt)
函数将使用 ISampleGrabber::GetCurrentBuffer()
方法获取缓冲区并将其保存为 BMP。
BOOL CVideoImageCapDlg::SaveBufferToBitmap(AM_MEDIA_TYPE mt)
{
// Find the required buffer size.
long cbBuffer = 0;
HRESULT hr = m_pGrabber->GetCurrentBuffer(&cbBuffer, NULL);
switch(hr)
{
case E_INVALIDARG:
AfxMessageBox("E_INVALIDARG");
break;
case E_OUTOFMEMORY:
AfxMessageBox("E_OUTOFMEMORY");
break;
case E_POINTER:
AfxMessageBox("E_POINTER");
break;
case VFW_E_NOT_CONNECTED:
AfxMessageBox("VFW_E_NOT_CONNECTED");
break;
case VFW_E_WRONG_STATE:
AfxMessageBox("VFW_E_WRONG_STATE");
break;
}
if(FAILED(hr))
return FALSE;
char *pBuffer = new char[cbBuffer];
if (!pBuffer)
{
// Out of memory. Return an error code.
return FALSE;
}
hr = m_pGrabber->GetCurrentBuffer(&cbBuffer, (long*)pBuffer);
ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
hr = m_pGrabber->GetConnectedMediaType(&mt);
if (FAILED(hr))
{
// Return error code.
return FALSE;
}
// Examine the format block.
VIDEOINFOHEADER *pVih;
if ((mt.formattype == FORMAT_VideoInfo) &&
(mt.cbFormat >= sizeof(VIDEOINFOHEADER)) &&
(mt.pbFormat != NULL) )
{
pVih = (VIDEOINFOHEADER*)mt.pbFormat;
}
else
{
return FALSE;
}
//
// Save image data as Bitmap.
// This is just to make this sample easily understandable.
//
HANDLE fh;
BITMAPFILEHEADER bmphdr;
DWORD nWritten;
memset(&bmphdr, 0, sizeof(bmphdr));
bmphdr.bfType = ('M' << 8) | 'B';
bmphdr.bfSize = sizeof(bmphdr) + sizeof(BITMAPINFOHEADER) + cbBuffer;
bmphdr.bfOffBits = sizeof(bmphdr) + sizeof(BITMAPINFOHEADER);
fh = CreateFile(m_strSaveTo,
GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
WriteFile(fh, &bmphdr, sizeof(bmphdr), &nWritten, NULL);
WriteFile(fh,
&pVih->bmiHeader,
sizeof(BITMAPINFOHEADER), &nWritten, NULL);
WriteFile(fh, pBuffer, cbBuffer, &nWritten, NULL);
CloseHandle(fh);
free(pBuffer);
return TRUE;
}
我们还可以通过设置 ISampleGrabber->SetCallback()
方法来调用回调函数来捕获媒体样本。FrameGrabCallback
是一个派生自 ISampleGrabberCB
的类。下面是一个回调类的示例。请注意,该类实现了 IUnknown
,它通过 ISampleGrabber
接口继承,但它不维护引用计数。这是安全的,因为应用程序在堆栈上创建对象,并且对象在过滤器图的整个生命周期内都保持在其范围内。所有工作都在 BufferCB
方法中完成,该方法在 Sample Grabber 获得新样本时由其调用。在下面的示例中,该方法将位图写入文件。
//
// Callback class for Sample Grabber filter.
//
class FrameGrabCallback : public ISampleGrabberCB
{
public :
// Fake out any COM ref counting
//
STDMETHODIMP_(ULONG) AddRef() { return 2; }
STDMETHODIMP_(ULONG) Release() { return 1; }
STDMETHODIMP QueryInterface(REFIID iid, void** ppv)
{
if (NULL == ppv)
return E_POINTER;
*ppv = NULL;
if (IID_IUnknown == iid)
{
*ppv = (IUnknown*)this;
AddRef();
return S_OK;
}
else
if (IID_ISampleGrabberCB == iid)
{
*ppv = (ISampleGrabberCB*)this;
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
// Constructor/destructor
FrameGrabCallback()
{
framenum=0;
}
~FrameGrabCallback() {}
public:
// These will get set by the main thread below. We need to
// know this in order to write out the bmp
long Width;
long Height;
long framenum;
CString strPath;
STDMETHODIMP SampleCB(double n,IMediaSample *pms)
{
return 0;
}
// The sample grabber is calling us back on its deliver thread.
// This is NOT the main app thread!
//
STDMETHODIMP BufferCB( double SampleTime, BYTE * pBuffer, long BufferSize )
{
//
// Convert the buffer into a bitmap
// strPath ="path"; eg : "c://Test.bmp"
TCHAR szFilename[MAX_PATH];
if(strPath=="")
{
wsprintf(szFilename, TEXT("bitmap%ld.bmp\0"), framenum );
}
else
{
wsprintf(szFilename, strPath , framenum );
}
framenum++;
// Create a file to hold the bitmap
HANDLE hf = CreateFile(szFilename, GENERIC_WRITE, FILE_SHARE_READ,
NULL, CREATE_ALWAYS, NULL, NULL );
if( hf == INVALID_HANDLE_VALUE )
{
_tprintf( TEXT("INVALID_HANDLE_VALUE\r\n"));
}
/*_tprintf(TEXT("Found a sample at time %ld ms\t[%s]\r\n"),
long(SampleTime*1000), szFilename );*/
// Write out the file header
//
BITMAPFILEHEADER bfh;
memset( &bfh, 0, sizeof( bfh ) );
bfh.bfType = 'MB';
bfh.bfSize = sizeof( bfh ) + BufferSize + sizeof( BITMAPINFOHEADER );
bfh.bfOffBits = sizeof( BITMAPINFOHEADER ) + sizeof( BITMAPFILEHEADER );
DWORD Written = 0;
WriteFile( hf, &bfh, sizeof( bfh ), &Written, NULL );
// Write the bitmap format
//
BITMAPINFOHEADER bih;
memset( &bih, 0, sizeof( bih ) );
bih.biSize = sizeof( bih );
bih.biWidth = Width;
bih.biHeight = Height;
bih.biPlanes = 1;
bih.biBitCount = 24;
Written = 0;
WriteFile( hf, &bih, sizeof( bih ), &Written, NULL );
// Write the bitmap bits
//
Written = 0;
WriteFile( hf, pBuffer, BufferSize, &Written, NULL );
CloseHandle( hf );
return 0;
}
};
创建此类后,请在运行图之前使用以下代码。一次只使用两种方法之一(回调或获取当前缓冲区)。
// Set the callback, so we can grab the one sample
//Also set the video width and height to grab the frame.
FrameGrabCallback m_FrameGrabCallback;
m_FrameGrabCallback.Width=320;
m_FrameGrabCallback.Height=240;
m_FrameGrabCallback.framenum=1;
hr = m_pGrabber->SetCallback( &m_FrameGrabCallback, 1 );//Set the callback method
完成所有工作后,我们可以停止图,取消回调函数的订阅,并释放所有资源。
m_pGrabber->SetCallback(NULL,1);
pControl->Stop();
pControl->Release();
m_pGrabber->Release();
pEvent->Release();
pGraph->Release();
pGrabberF->Release();
参考
历史
- First version.