学习DShow Filter的开发,主要针对于SourceFilter、TransInPlaceFilter和RenderFilter方面。Filter的开发并不需要我们从头到尾的编写代码,DShow为我们提供了许多类,通过这些类我们可以快速开发出良好的Filter。下面就这几种Filter的学习总结如下。
在DirectShow中,Filter一般被分成三类:SourceFilter、TransformFilter和RenderFilter。区分这三种Filter最直观的是区分它们所包含的Pin。SourceFilter有至少一个的输出Pin,而没有输入Pin;TransformFilter至少有一个输入Pin和至少一个的输出Pin。RenderFilter至少有一个输入Pin。那么什么是Pin呢?Pin和Filter一样,都是一种Com组件。Filter可以被看成是一个容器,每个Filter都至少包含一个Pin。而Pin的工作的就是连接两个Filter,传输和处理数据。两个Filter之间的连接其实就是两个Pin之间的连接。如下图:
而2个Pin之间连接过程实际上是Pin之间媒体类型的协商过程,简单来说:
1.调用输出PIN上的Connect方法
2.调用输入PIN上的ReceiveConnection方法
3.如果这两个方法都成功了,那么则连接成功。
当完成上面的步骤后,两个Filter之间还不能传输数据,因为还没有为它们分配内存空间。当上面步骤成功完成后:
4.在输出PIN上调用CompleteConnect方法,而这个方法就是两个Filter之间的内存的分配与管理。
这样,两个Filter之间就可以进行数据传输了。但是,两个Filter之间的数据是以什么样的形式、如何进行传输的呢?
Filter之间的数据传输是通过Pin来实现的。在连接着的2个PIN之间有一个Allocator。它是一个Sample分配器。它创建、管理一个或多个Sample。而这个Sample可以看做是数据的载体。我们传输的数据被封装在Sample中,数据传送时,输出PIN会调用IMemAllocator::GetBuffer方法得到一个Sample,然后我们可调用IMediaSample::GetPointer来得到这个Sample的内存地址,把数据填充好,然后传送给下一级Filter的输入PIN。
那么这个Sample是如何传送的呢?数据的传输方式有两种:推模式(Push mode)和拉模式(Pull mode)。
(一)推模式
推模式在DShow中主要用于实时源,如摄像头图像的采集。与它连接的下级Filter需要实现的Pin是IMemInputPin接口。它在数据传输时是主动的向下级Filter“推”数据。首先SourceFilter完成了数据的读取,然后会主动的调用下级Filter上IMemInputPin接口的IMemInputPin::Receive或IMemInputPin::ReceiveMultiple方法来传递数据。
(二)拉模式
拉模式在DShow中主要用于文件源,如一个视频文件的回放。要使用拉模式需要实现IAsyncReader接口。它的数据传输的时间和推模式相反。它是被动的提供数据。在需要数据时,由它上级的Filter的输入Pin主动来请求数据,调用IAsyncReader接口上的方法,它则被动的去执行。IAsyncReader数据处理方法有三个:IAsyncReader::Request、IAsyncReader::SyncReadAligned、IAsyncReader::SyncRead。第一个是异步的,后二个是同步的。
当然,推/拉模式只是数据传输的两种方式,开发过程中你可以任意的选择。但是还是推荐按照DShow建议:推模式在实时源时使用,拉模式在文件源时使用。因为这样有时会减少许多不必要的工作。
最后,当一个SourceFilter结束发送数据流时,它调用和它连接的filter的输入pin的IPin::EndOfStream,然后下游的filter再依次通知与之相连的filter。当EndOfStream方法一直调用到renderer filter的时候,最后的一个filter就给filter图表管理器发送一个EC_COMPLETE事件通知。如果renderer有多个输入pin,当所有的输入pin都接收到end of stream通知的时候,它才会给filter图表管理器发送一个EC_COMPLETE事件通知。在一些情况下,下游的filter可能比SourceFilter更早的发现数据流的结束。在这种情况下,下游filter发送 结束stream的通知,同时, IMemInputPin::Receive函数返回S_FALSE直到图表管理器停止。这个返回值提示源filter停止发送数据。缺省的情况下,filter图表管理器并不将EC_COMPLETE事件通知发送给应用程序,当所有的数据流都发送了EC_COMPLETE事件通知后,它才给应用程序发送一个EC_COMPLETE事件通知。所以,应用程序只有在所有的数据流停止的时候才能接收到这个通知。
简单来说SourceFilter就是一个提供数据的Filter。这个数据可以是本地音视频流、图片数据、实时采集数据等。
这里将分别给出推模式和拉模式的例子。DShow提供了许多类供我们开发Filter,我们可以从这些基类中派生出我们的新类,从而简化开发流程。
虽然说推模式主要用于实时源,但这里给的例子是用拉模式推送文件源。这个Filter的功能就是向下一级Filter主动的推送图片数据,最后由系统的RenderFilter显示出来。
不同Filter的注册都是大同小异的。创建至少一个GUID,然后在CPP写上下面代码(红色部分按需修改):
const AMOVIESETUP_MEDIATYPE sudPinTypes =
{
&MEDIATYPE_NULL, // Major type
&MEDIASUBTYPE_NULL // Minor type
};
const AMOVIESETUP_PIN sudPins =
{
L"Output", // Pin string name(Pin的名字)
FALSE, // Is it rendered()
TRUE, // Is it an output(输出还是输入Pin?)
FALSE, // Allowed none
FALSE, // Likewise many
&CLSID_NULL, // Connects to filter
L"Input", // Connects to pin
1, // Number of types
&sudPinTypes // Pin information
};
const AMOVIESETUP_FILTER sudFilter =
{
&CLSID_SyncSourceFilter, // Filter CLSID
L"SyncSourceFilter", // String name
MERIT_DO_NOT_USE, // Filter merit
1, // Number pins
&sudPins // Pin details
};
CFactoryTemplate g_Templates[]= {
L"SyncSourceFilter", &CLSID_SyncSourceFilter, CFileSource::CreateInstance, NULL, &sudFilter
};
int g_cTemplates = 1;//个数
2.首先为SourceFilter选择一个基类,这里选择CSource。SourceFilter包含一个或多个输出Pin(这里为1个)。这个输出Pin派生于CSourceStream(这里为CFileStream)。
要实现SourceFilter上的输出Pin,需要:
后面具体说明。
3.在SourceFilter类中实现
static CUnknown * WINAPI CreateInstance(IUnknown *pUnk, HRESULT *phr)
{
CFileSource *pNewFilter = new CFileSource(pUnk, phr );
if (phr)
{
if (pNewFilter == NULL)
*phr = E_OUTOFMEMORY;
else
*phr = S_OK;
}
return pNewFilter;
}
4. 在SourceFilter类中定义一个CSourceStream的指针成员变量m_pPin,并在SourceFilter类的构造函数中创建输出Pin的对象。并构造基类。
CFileSource::CFileSource(IUnknown *pUnk, HRESULT *phr)
: CSource(NAME("SyncFileSource"), pUnk, CLSID_SyncSourceFilter)
{
m_pPin = new CFileStream(phr, this);
if (phr)
{
if (m_pPin == NULL)
*phr = E_OUTOFMEMORY;
else
*phr = S_OK;
}
}
SourceFilter类到这里就完成了,下面来看CFileStream类。
1.在CFileStream的头文件中添加成员变量:
BITMAPINFO *m_pBitInfo; //指向BMP信息结构体指针
DWORD m_cbBitmapInfo; //BMP信息结构体大小
HANDLE m_hFile;
BYTE * m_pFile; //指向文件数据的开头
BYTE * m_pImageData; //指向文件中图片数据(不包含BMP头)的开头
int m_iFrameNumber;//帧数
const REFERENCE_TIME m_rtFrameLength;//每秒帧数
CCritSec m_csPin;//临界区
2.在CFileStream的构造函数中初始化:
CFileStream::CFileStream(HRESULT *phr, CSource *pFilter)
: CSourceStream(NAME("SyncFileSourceOutPin"), phr, pFilter, L"Out"),//基类构造
m_pBitInfo(0),
m_cbBitmapInfo(0),
m_hFile(INVALID_HANDLE_VALUE),
m_pFile(NULL),
m_pImageData(NULL),
m_iFrameNumber(0),
m_rtFrameLength(10000000/5)
{
m_hFile = CreateFile(L"d:\\1.bmp", GENERIC_READ, 0, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (m_hFile == INVALID_HANDLE_VALUE)
{
*phr =S_FALSE;
return;
}
DWORD dwFileSize = GetFileSize(m_hFile, NULL);
if (dwFileSize == INVALID_FILE_SIZE)
{
*phr =S_FALSE;
return;
}
m_pFile = new BYTE[dwFileSize];
if(!m_pFile)
{
*phr = E_OUTOFMEMORY;
return;
}
DWORD nBytesRead = 0;
if(!ReadFile(m_hFile, m_pFile, dwFileSize, &nBytesRead, NULL))
{ *phr =S_FALSE;
return;
}
int cbFileHeader = sizeof(BITMAPFILEHEADER);
BITMAPFILEHEADER *pBm = (BITMAPFILEHEADER*)m_pFile;
m_cbBitmapInfo = pBm->bfOffBits - cbFileHeader;
m_pBitInfo = (BITMAPINFO*)(m_pFile + cbFileHeader);//取得BMP图片信息起始地址
m_pImageData = m_pFile + cbFileHeader + m_cbBitmapInfo;//取得BMP图片的数据
CloseHandle(m_hFile);
m_hFile = INVALID_HANDLE_VALUE;
}
3.实现 CFileStream::GetMediaType方法:
因为我们写的是SourceFilter,因此我们需要在这里为我们推送的数据设置好它的数据类型。需要说明的是在2个Pin连接的时候,双方都会调用GetMediaType和CheckMediaType方法来检测双方的媒体类型,当双方都对某种媒体类型达成一致时,那么这个媒体类型协商的过程就完成了。
在DShow中,媒体类型是用来描述一定格式的数据流。它是一个结构体:
typedef struct _MediaType {
GUID majortype;
GUID subtype;
BOOL bFixedSizeSamples;
BOOL bTemporalCompression;
ULONG lSampleSize;
GUID formattype;
IUnknown *pUnk;
ULONG cbFormat;
[size_is(cbFormat)] BYTE *pbFormat;
} AM_MEDIA_TYPE;
从上可以看出媒体类型主要分3类:majortype:主类型
subtype:辅助格式说明
formattype:格式细节说明
实现 CFileStream::GetMediaType:
HRESULT CFileStream::GetMediaType(CMediaType *pMediaType)
{
CAutoLock cAutoLock(m_pFilter->pStateLock());
CheckPointer(pMediaType, E_POINTER);
if (!m_pImageData)
return E_FAIL;
VIDEOINFOHEADER *pvi = (VIDEOINFOHEADER*)pMediaType->AllocFormatBuffer(SIZE_PREHEADER + m_cbBitmapInfo);//这里为pMediaType分配格式数据块,如果数据块是存在的那么就释放。
if (pvi == 0)
return(E_OUTOFMEMORY);
ZeroMemory(pvi, pMediaType->cbFormat);
pvi->AvgTimePerFrame = m_rtFrameLength; //每秒帧
memcpy(&(pvi->bmiHeader), m_pBitInfo, m_cbBitmapInfo);
pvi->bmiHeader.biSizeImage = GetBitmapSize(&pvi->bmiHeader);
SetRectEmpty(&(pvi->rcSource));
SetRectEmpty(&(pvi->rcTarget));
pMediaType->SetType(&MEDIATYPE_Video);//设置主类型
pMediaType->SetFormatType(&FORMAT_VideoInfo);
pMediaType->SetTemporalCompression(FALSE);
const GUID SubTypeGUID = GetBitmapSubtype(&pvi->bmiHeader);
pMediaType->SetSubtype(&SubTypeGUID);
pMediaType->SetSampleSize(pvi->bmiHeader.biSizeImage);
return S_OK;
}
4. CFileStream::DecideBufferSize实现
从函数名字就可看出这个函数要做什么事:决定一个Sample的大小
HRESULT CFileStream::DecideBufferSize(IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pRequest)
{
HRESULT hr;
CAutoLock cAutoLock(m_pFilter->pStateLock());
CheckPointer(pAlloc, E_POINTER);
CheckPointer(pRequest, E_POINTER);
if (!m_pImageData)
return E_FAIL;
VIDEOINFOHEADER *pvi = (VIDEOINFOHEADER*) m_mt.Format();
if (pRequest->cBuffers == 0)
{
pRequest->cBuffers = 2;
}
pRequest->cbBuffer = pvi->bmiHeader.biSizeImage;//这里就是Buffer的大小,这里取的是BMP图片大小
ALLOCATOR_PROPERTIES Actual;
hr = pAlloc->SetProperties(pRequest, &Actual);
if (FAILED(hr))
{
return hr;
}
if (Actual.cbBuffer < pRequest->cbBuffer)
{
return E_FAIL;
}
return S_OK;
}
5.实现CFileStream::FillBuffer (IMediaSample *pSample)
在这个函数中,我们就要为pSample填充数据,使用pSample->GetPointer可以得到内存指针,使用pSample->GetSize可以得到大小。
HRESULT CFileStream::FillBuffer(IMediaSample *pSample)
{
BYTE *pData;
long cbData;
CheckPointer(pSample, E_POINTER);
if (!m_pImageData)
return E_FAIL;
CAutoLock cAutoLockShared(&m_csPin);
pSample->GetPointer(&pData);
cbData = pSample->GetSize();
ASSERT(m_mt.formattype == FORMAT_VideoInfo);
VIDEOINFOHEADER *pVih = (VIDEOINFOHEADER*)m_mt.pbFormat;
memcpy(pData, m_pImageData, min(pVih->bmiHeader.biSizeImage, (DWORD) cbData));
REFERENCE_TIME rtStart = m_iFrameNumber * m_rtFrameLength;
REFERENCE_TIME rtStop = rtStart + m_rtFrameLength;
pSample->SetTime(&rtStart, &rtStop);
m_iFrameNumber++;
pSample->SetSyncPoint(TRUE);
return S_OK;
}
到这里,一个推模式的SourceFilter就完了。纵观整体,不难发现实现这个Filter我们所要完成的事仅仅是重写几个函数: CSourceStream::GetMediaType、CBaseOutputPin::DecideBufferSize、CSourceStream::FillBuffer 。还是非常简单的。当然,这几个方法是最少也是必须重写的,还有一些方法还是可以重写,具体的方法可参见MSDN。
(二)拉模式的SourceFilter(视频播放)
拉模式的SourceFilter要比推模式的SourceFilter要复杂一些,但是通过基类来完成也并不是很困难。在拉模式中,Filter推荐使用CBaseFilter,Pin推荐使用CBasePin,当然IAsyncReader接口是必须要实现的。
1.从CBaseFilter派生一个新类
2.为这个新类添加一个Pin的成员变量,这个Pin必须是继承CBasePin而来。
3.重写纯虚函数CBaseFilter::GetPin来返回这个Filter上的Pin。
4.重写纯虚函数CBaseFilter::GetPinCount,返回这个Filter上的Pin的个数。
1. CBasePin::CheckMediaType ,在Pin连接时检查媒体类型时调用
2. CBasePin::GetMediaType ,在Pin连接时取得媒体类型时调用,你可以在这里修改媒体类型。
3.IPin::BeginFlush,开始清除数据。这个方法我们不需要手动调用,由其他Filter来调用以清除graph中的数据。
4. IPin::EndFlush,结束清除数据。同BeginFlush类似。
1.IAsyncReader::BeginFlush,IAsyncReader::EndFlush,类似上面的。
2. IAsyncReader::Length,返回数据流的总长度以及可用长度。
3. IAsyncReader::RequestAllocator,在Pin连接的时候会调用它,当Pin的媒体类型协商成功之后,为Pin之间分配和管理内存的。
4. IAsyncReader::Request,异步请求数据,需要字节对齐。
5. IAsyncReader::SyncReadAligned,同步对齐请求数据,需要字节对齐。
6. IAsyncReader::SyncRead,同步请求数据,不需要字节对齐。
7. IAsyncReader::WaitForNext,等待请求的执行完成