使用不同的 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.


