Introduction
This article explains how to grab a frame from a streaming URL (like a stream from WME) and save it as a BMP using DirectShow. In this article, I am using the IBase
filter for the WM ASF Reader (network reader) for reading the network resource, and the ISampleGrabber
filter to grab the bitmap frame. Here, I have tried to explain two methods (SetCallBack
and GetCurrentBuffer
) to grab a bitmap using the ISampleGrabber
filter.
Background
In my recent project, I met with this same problem, capturing a snapshot from a WME streaming URL. I searched about it in the web and saw a lot of articles to capture images from a movie file or something like that, but article about how to capture an image from a streaming URL. I implemented this using some references from MSDN and the web. Initially, I implemented a solution by using the IFileSourceFilter
and the GetCurrentBuffer
method in the ISampleGrabber
interface. That was enough for my needs. After that, I tried the CallBack
(SetCallBack
) in the ISampleGrabber
interface for a different experience. I have explained these two modes in here. I will try to explain what I have learned and implemented. I hope the sample code will support someone in solving their problem.
Setting Up Your Visual Studio Project
You need to add header files from the Windows Platform SDK and the DShow base classes to your include path. The project has to be linked with Strmbase.lib.
#include "qedit.h" // SampleGrabber filter
#include "atlbase.h" // for using atl support
#include "dshow.h" // DirectShow header
Using the Code
The following DShow interfaces will be used in this article...
IGraphBuilder *pGraph = NULL; IMediaControl *pControl = NULL; IMediaEvent *pEvent = NULL; IBaseFilter *pWMASFReader = NULL; IPin *pStreamOut = NULL,*pStreamRender = NULL; IFileSourceFilter *pIFileSourceFilter = NULL;
First, initialize COM and create the filter graph manager.
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;
Create the WM ASF Reader filter for network reading, and add it to the graph. Then, query for the file source filter to specify the network location, and load the specified network location (e.g.: http://host:port) using the IFileSourceFilter::Load()
method. It will load the desired network source; if the network source is not found or is invalid, then it will return an HRESULT
value with the result code. m_strURL
is a CString
variable that contains the desired network location.
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))
{
hr = pIFileSourceFilter->Load(m_strURL.AllocSysString(), NULL);
}
}
}
if(FAILED(hr))
return FALSE;
Now we have the network source with us. We need to create the Sample Grabber filter and get the Sample Grabber interface. Create the Sample Grabber filter, add it to the graph, and query for the ISampleGrabber
(ISampleGrabber *m_pGrabber
).
IBaseFilter *pGrabberF = NULL;
hr = CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER,
IID_IBaseFilter, (void**)&pGrabberF);
if (FAILED(hr))
{
return FALSE;
}
hr = pGraph->AddFilter(pGrabberF, L"Sample Grabber");
if (FAILED(hr))
{
return FALSE;
}
pGrabberF->QueryInterface(IID_ISampleGrabber, (void**)&m_pGrabber);
We need to specify the media type for the connection on the input pin of the Sample Grabber. For that, we will use the AM_MEDIA_TYPE
structure.
AM_MEDIA_TYPE mt;
ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
mt.majortype = MEDIATYPE_Video;
mt.subtype = MEDIASUBTYPE_RGB24;
hr = m_pGrabber->SetMediaType(&mt);
You can add a null renderer filter in your graph to prevent the preview window from being displayed, if you need. The null renderer filter is a renderer that discards every sample it receives, without displaying or rendering the sample data. Otherwise, you can query IVideoWindow
from the graph and can handle the popup window.
IBaseFilter *pNullRenderer;
hr = CoCreateInstance (CLSID_NullRenderer, NULL, CLSCTX_INPROC_SERVER,
IID_IBaseFilter, (void **)&pNullRenderer);
hr = pGraph->AddFilter(pNullRenderer, L"Null Renderer");
Now we will connect the network reader filter to the grabber filter. The Sample Grabber is a transform filter, so the output pin must be connected to another filter. Often, you may simply want to discard the samples after you are done with them. In that case, connect the Sample Grabber to the null renderer filter, which discards the data that it receives.
hr = ConnectFilters(pGraph, pWMASFReader, pGrabberF);
The following function will connect the first filter to the second filter:
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;
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;
}
Then, get the output pin and render the stream.
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;
}
Now query the Media control for running the graph and the Media event for retrieving event notifications.
hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);
if(FAILED(hr))
return FALSE;
hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
if(FAILED(hr))
return FALSE ;
The Sample Grabber operates in one of two modes:
- Buffering mode makes a copy of each sample before delivering the sample downstream.
- Callback mode invokes an application-defined callback function on each sample.
Call the ISampleGrabber::SetOneShot
method with the value FALSE
. This causes the Sample Grabber to continue the running state of the graph after it receives the first media sample. And, ISampleGrabber::SetBufferSamples
with the value TRUE
to specify whether to copy sample data into a buffer as it goes through the filter. So that you can get the copied buffer by calling ISampleGrabber::GetCurrentBuffer
.
hr = m_pGrabber->SetOneShot(FALSE);
hr = m_pGrabber->SetBufferSamples(TRUE);
If everything goes fine, then we can run the graph.
long evCode=0;
hr=pControl->Run();
hr=pEvent->WaitForCompletion(INFINITE, &evCode); Sleep(1000);
If the graph is running perfectly, then we can retrieve the current buffer by using the ISampleGrabber::GetCurrentBuffer()
method. The SaveBufferToBitmap(mt)
function will grab the buffer using the ISampleGrabber::GetCurrentBuffer()
method and will save it as BMP.
BOOL CVideoImageCapDlg::SaveBufferToBitmap(AM_MEDIA_TYPE mt)
{
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)
{
return FALSE;
}
hr = m_pGrabber->GetCurrentBuffer(&cbBuffer, (long*)pBuffer);
ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
hr = m_pGrabber->GetConnectedMediaType(&mt);
if (FAILED(hr))
{
return FALSE;
}
VIDEOINFOHEADER *pVih;
if ((mt.formattype == FORMAT_VideoInfo) &&
(mt.cbFormat >= sizeof(VIDEOINFOHEADER)) &&
(mt.pbFormat != NULL) )
{
pVih = (VIDEOINFOHEADER*)mt.pbFormat;
}
else
{
return FALSE;
}
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;
}
We can also grab the media sample by invoking the callback function by setting the ISampleGrabber->SetCallback()
method. FrameGrabCallback
is a class derived from ISampleGrabberCB
. What follows is an example of the callback class. Note that the class implements IUnknown
, which it inherits through the ISampleGrabber
interface, but it does not keep a reference count. This is safe because the application creates the object on the stack, and the object remains in scope throughout the lifetime of the filter graph. All of the work happens in the BufferCB
method, which is called by the Sample Grabber whenever it gets a new sample. In the following example, the method writes the bitmap to a file:
class FrameGrabCallback : public ISampleGrabberCB
{
public :
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;
}
FrameGrabCallback()
{
framenum=0;
}
~FrameGrabCallback() {}
public:
long Width;
long Height;
long framenum;
CString strPath;
STDMETHODIMP SampleCB(double n,IMediaSample *pms)
{
return 0;
}
STDMETHODIMP BufferCB( double SampleTime, BYTE * pBuffer, long BufferSize )
{
TCHAR szFilename[MAX_PATH];
if(strPath=="")
{
wsprintf(szFilename, TEXT("bitmap%ld.bmp\0"), framenum );
}
else
{
wsprintf(szFilename, strPath , framenum );
}
framenum++;
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"));
}
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 );
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 );
Written = 0;
WriteFile( hf, pBuffer, BufferSize, &Written, NULL );
CloseHandle( hf );
return 0;
}
};
After the creation of this class, please use the following code before running the graph. Only use one of the two methods (callback or get current buffer) at a time.
FrameGrabCallback m_FrameGrabCallback;
m_FrameGrabCallback.Width=320;
m_FrameGrabCallback.Height=240;
m_FrameGrabCallback.framenum=1;
hr = m_pGrabber->SetCallback( &m_FrameGrabCallback, 1 );
After everything is done, we can stop the graph, unsubscribe the callback function, and release all the resources.
m_pGrabber->SetCallback(NULL,1);
pControl->Stop();
pControl->Release();
m_pGrabber->Release();
pEvent->Release();
pGraph->Release();
pGrabberF->Release();
Reference
History