DirectShow20191010
DirectShow应用开发过程
1.需要包含的头文件
DirectShow SDK建议,所有DirectShow应用程序都应该包含Dshow.h文件,Dshow.h嵌套
包含其他一些头文件:
P127
在实际应用中,我们也常常用streams.h文件来替换Dshow.h文件。
2.需要连接的库文件
DirectShow SDK建议,DirectShow应用程序应该至少连接库文件Strmiids.lib和Quartz.lib。
前者定义了DirectShow标准的CLSID和IID,后者定义了导出函数AMGetErrorText(如果应用程序中
没有使用这个函数,也可以不连接这个库)。如果我们写程序的时候包含了头文件streams.h,则
一般库文件还要连接strmbased.lib(Release版本为strmbase.lib)、uuid.lib和winmm.lib。
3.VC的系统编译环境
确认DirectX SDK的Include目录和Lib目录都已经加入了VC的系统编译环境,并且放在标准的VC
目录之前,以保证编译器能够拿到最新版本的源文件。
因为DirectShow应用程序时一种COM客户程序,因此在调用任何COM函数之前调用CoInitialize
(或ColnitializeEx)函数进行COM库的初始化(一般是在应用程序启动的时候调用一次),在结束
COM库使用时调用CoUninitialize函数进行反初始化(一般是在应用程序退出前调用一次)。
4.一般开发过程
第一阶段,创建一个Filter Graph Manager组件,代码如下:
IGraphBuilder *pGraph=NULL;
HRESULT hr=CoCreateInstance(CLSID_FilterGraph,NULL,
CLSCTX_INPROC_SERVER,IID_IGraphBuilder,(void **)&pGraph);
第二阶段,根据实际的应用,创建一条完整的Filter链路。比如播放一个本地文件,最简单快速
代码如下:
hr=pGraph->RenderFile(L"C:\\Example.avi",NULL);
第三阶段,调用Filter Graph Manager上(或者直接在某个Filter上)的各个接口方法进行控制,
并且完成Filter Graph Manager 与应用程序的事件交互。比如调用IMediaControl接口方法控制Filter
Graph的状态转换,代码如下:
IMediaControl *pControl=NULL;
hr=pGraph->QueryInterface(IID_IMediaControl,(void **)&pControl);
hr=pControl->Run(); //运行Filter Graph
通用Filter Graph构建技术
1.加入一个指定CLSID的Filter
给定了一个Filter的CLSID,我们就可以使用COM API函数CoCreateInstance来创建它,并且使用
IFilterGraph::AddFilter接口方法将其加入到Filter Graph中,代码如下:
HRESULT AddFilterByCLSID(
IGraphBuilder *pGraph, //Pointer to the Filter Graph Manager
const GUID &clsid, //CLSID of the filter to create
LPCWSTR wszName, //A name for the filter
IBaseFilter **ppF) //Receives a pointer to the filter
{
if(!pGraph||!ppF)
{
return E_POINTER;
}
*ppF=0;
IBaseFilter *pF=0;
HRESULT hr=CoCreateInstance(clsid,0,CLSCTX_INPROC_SERVER,
IID_IBaseFilter,reinterpret_cast<void **>(&pF));
if(SUCCEEDED(hr))
{
hr=pGraph->AddFilter(pF,wszName);
if(SUCCEEDED(hr))
{
*ppF=pF;
}
else
{
pF->Release();
}
}
return hr;
}
我们现在要向Filter Graph中加入一个AVI Mux Filter,代码如下:
IBaseFilter *pMux;
hr=AddFilterByCLSID(pGraph,CLSID _AviDest,L"AVI Mux",&pMux);
if(SUCCEEDED(hr))
{
/*...*/
pMux->Release();
}
并不是所有的Filter都能通过CoCreateInstance函数来创建,大部分注册在Audio Compressors目录下
的Filter(它们使用了CLSID_ACMWrapper包装Filter)、大部分注册在Video Compressors目录下的Filter
(它们使用了CLSID_AVICo包装Filter),以及代表硬件(WDM或VFW)的包装Filter,必须通过枚举的方式
来创建。
2.得到Filter上的未连接Pin
当我们在程序中连接Filter的时候,首先是要取得Filter上未连接的输入Pin或者输出Pin。方法是,枚
举Filter上的所有Pin,调用IPin::QueryDirection查看Pin的方向,调用IPin::ConnectedTo查看Pin的连接
状态。代码如下:
HRESULT GetUnconnectedPin(
IBaseFilter *pFilter, //Pointer to the filter
PIN_DIRECTION PinDir, //Direction of the pin to find
IPin **ppPin) //Receives a pointer to the pin
{
*ppPin=0;
IEnumPins *pEnum=0;
IPin *pPin=0;
HRESULT hr=pFilter->EnumPins(&pEnum);
if(FAILED(hr))
{
return hr;
}
while(pEnum->Next(1,&pPin,NULL)==S_OK)
{
PIN_DIRECTION ThisPinDir;
pPin->QueryDirection(&ThisPinDir);
if(ThisPinDir==PinDir)
{
IPin *pTmp=0;
hr=pPin->ConnectedTo(&pTmp);
if(SUCCEEDED(hr)) //Already connected, not the pin we want
{
pTmp->Release();
}
else //Unconnected, this is the pin we want
{
pEnum->Release();
*ppPin=pPin;
return S_OK;
}
}
pPin->Release();
}
pEnum->Release();
//Did not find a matching pin
return E_FAIL;
}
比如我们现在要得到一个Filter的未连接的输出Pin,代码如下:
IPin *pOut=NULL;
HRESULT hr=GetUnconnectedPin(pFilter,PINDIR_OUTPUT,&pOut);
if(SUCCEEDED(hr))
{
/*...*/
pOut->Release();
}
1.需要包含的头文件
DirectShow SDK建议,所有DirectShow应用程序都应该包含Dshow.h文件,Dshow.h嵌套
包含其他一些头文件:
P127
在实际应用中,我们也常常用streams.h文件来替换Dshow.h文件。
2.需要连接的库文件
DirectShow SDK建议,DirectShow应用程序应该至少连接库文件Strmiids.lib和Quartz.lib。
前者定义了DirectShow标准的CLSID和IID,后者定义了导出函数AMGetErrorText(如果应用程序中
没有使用这个函数,也可以不连接这个库)。如果我们写程序的时候包含了头文件streams.h,则
一般库文件还要连接strmbased.lib(Release版本为strmbase.lib)、uuid.lib和winmm.lib。
3.VC的系统编译环境
确认DirectX SDK的Include目录和Lib目录都已经加入了VC的系统编译环境,并且放在标准的VC
目录之前,以保证编译器能够拿到最新版本的源文件。
因为DirectShow应用程序时一种COM客户程序,因此在调用任何COM函数之前调用CoInitialize
(或ColnitializeEx)函数进行COM库的初始化(一般是在应用程序启动的时候调用一次),在结束
COM库使用时调用CoUninitialize函数进行反初始化(一般是在应用程序退出前调用一次)。
4.一般开发过程
第一阶段,创建一个Filter Graph Manager组件,代码如下:
IGraphBuilder *pGraph=NULL;
HRESULT hr=CoCreateInstance(CLSID_FilterGraph,NULL,
CLSCTX_INPROC_SERVER,IID_IGraphBuilder,(void **)&pGraph);
第二阶段,根据实际的应用,创建一条完整的Filter链路。比如播放一个本地文件,最简单快速
代码如下:
hr=pGraph->RenderFile(L"C:\\Example.avi",NULL);
第三阶段,调用Filter Graph Manager上(或者直接在某个Filter上)的各个接口方法进行控制,
并且完成Filter Graph Manager 与应用程序的事件交互。比如调用IMediaControl接口方法控制Filter
Graph的状态转换,代码如下:
IMediaControl *pControl=NULL;
hr=pGraph->QueryInterface(IID_IMediaControl,(void **)&pControl);
hr=pControl->Run(); //运行Filter Graph
通用Filter Graph构建技术
1.加入一个指定CLSID的Filter
给定了一个Filter的CLSID,我们就可以使用COM API函数CoCreateInstance来创建它,并且使用
IFilterGraph::AddFilter接口方法将其加入到Filter Graph中,代码如下:
HRESULT AddFilterByCLSID(
IGraphBuilder *pGraph, //Pointer to the Filter Graph Manager
const GUID &clsid, //CLSID of the filter to create
LPCWSTR wszName, //A name for the filter
IBaseFilter **ppF) //Receives a pointer to the filter
{
if(!pGraph||!ppF)
{
return E_POINTER;
}
*ppF=0;
IBaseFilter *pF=0;
HRESULT hr=CoCreateInstance(clsid,0,CLSCTX_INPROC_SERVER,
IID_IBaseFilter,reinterpret_cast<void **>(&pF));
if(SUCCEEDED(hr))
{
hr=pGraph->AddFilter(pF,wszName);
if(SUCCEEDED(hr))
{
*ppF=pF;
}
else
{
pF->Release();
}
}
return hr;
}
我们现在要向Filter Graph中加入一个AVI Mux Filter,代码如下:
IBaseFilter *pMux;
hr=AddFilterByCLSID(pGraph,CLSID _AviDest,L"AVI Mux",&pMux);
if(SUCCEEDED(hr))
{
/*...*/
pMux->Release();
}
并不是所有的Filter都能通过CoCreateInstance函数来创建,大部分注册在Audio Compressors目录下
的Filter(它们使用了CLSID_ACMWrapper包装Filter)、大部分注册在Video Compressors目录下的Filter
(它们使用了CLSID_AVICo包装Filter),以及代表硬件(WDM或VFW)的包装Filter,必须通过枚举的方式
来创建。
2.得到Filter上的未连接Pin
当我们在程序中连接Filter的时候,首先是要取得Filter上未连接的输入Pin或者输出Pin。方法是,枚
举Filter上的所有Pin,调用IPin::QueryDirection查看Pin的方向,调用IPin::ConnectedTo查看Pin的连接
状态。代码如下:
HRESULT GetUnconnectedPin(
IBaseFilter *pFilter, //Pointer to the filter
PIN_DIRECTION PinDir, //Direction of the pin to find
IPin **ppPin) //Receives a pointer to the pin
{
*ppPin=0;
IEnumPins *pEnum=0;
IPin *pPin=0;
HRESULT hr=pFilter->EnumPins(&pEnum);
if(FAILED(hr))
{
return hr;
}
while(pEnum->Next(1,&pPin,NULL)==S_OK)
{
PIN_DIRECTION ThisPinDir;
pPin->QueryDirection(&ThisPinDir);
if(ThisPinDir==PinDir)
{
IPin *pTmp=0;
hr=pPin->ConnectedTo(&pTmp);
if(SUCCEEDED(hr)) //Already connected, not the pin we want
{
pTmp->Release();
}
else //Unconnected, this is the pin we want
{
pEnum->Release();
*ppPin=pPin;
return S_OK;
}
}
pPin->Release();
}
pEnum->Release();
//Did not find a matching pin
return E_FAIL;
}
比如我们现在要得到一个Filter的未连接的输出Pin,代码如下:
IPin *pOut=NULL;
HRESULT hr=GetUnconnectedPin(pFilter,PINDIR_OUTPUT,&pOut);
if(SUCCEEDED(hr))
{
/*...*/
pOut->Release();
}
3.连接两个Filter
两个Filter的连接,总是从上一级Filter的输出Pin指向下一级Filter的输入Pin。
Filter Graph Manager提供的直接连接两个Filter的函数为IFilterGraph::ConnectDirect和IGraphBuilder::Connect。
参考代码如下:
HRESULT ConnectFilters(
IGraphBuilder *pGraph, //Filter Graph Manager
IPin *pOut, //Output pin on the upstream filter
IBaseFilter *pDest) //Downstream filter
{
if((pGraph==NULL)||(pOut==NULL)||(pDest==NULL))
{
return E_POINTER;
}
#ifdef debug
PIN_DIRECTION PinDir;
pOut->QueryDirection(&PinDir);
_ASSERTE(PinDir==PINDIR_OUTPUT);
#endif
//得到下一级Filter的输入Pin
IPin *pIn=0;
HRESULT hr=GetUnconnectedPin(pDest,PINDIR_INPUT,&pIn);
if(FAILED(hr))
{
return hr;
}
//将输出Pin连接到输入Pin
hr=pGraph->Connect(pOut,pIn);
pIn->Release();
return hr;
}
//不同参数的ConnectFilters函数重载形式
HRESULT ConnectFilters(
IGraphBuilder *pGraph,
IBaseFilter *pSrc,
IBaseFilter *pDest)
{
if((pGraph==NULL)||(pSrc==NULL)||(pDest==NULL))
{
return E_POINTER;
}
//Find an output pin on the first filter
IPin *pOut=0;
HRESULT hr=GetUnconnectedPin(pSrc,PINDIR_OUTPUT,&pOut);
if(FAILED(hr))
{
return hr;
}
hr=ConnectFilters(pGraph,pOut,pDest);
pOut->Release();
return hr;
}
比如我们现在要加入一个AVI Mux Filter和一个File Writer Filter,然后将它们相连起来。
代码如下:
IBaseFilter *pMux,*pWrite;
hr=AddFilterByCLSID(pGraph,CLSID_AviDest,L"AVI Mux",&pMux);
if(SUCCEEDED(hr))
{
hr=AddFilterByCLSID(pGraph,CLSID_FileWriter,L"File Writer",&pWrite);
if(SUCCEEDED(hr))
{
hr=ConnectFilters(pGraph,pMux,pWrite);
/*Use IFileSinkFilter to set the file name (not shown).*/
pWrite->Release();
}
pMux->Release();
}
4.查找Filter或Pin上的接口
//查找Filter上实现的某个接口
HRESULT FindFilterInterface(
IGraphBuilder *pGraph, //Pointer to the Filter Graph Manager
REFGUID iid, //IID of the interface to retrieve
void **ppUnk) //Receives the interface pointer
{
if(!pGraph||!ppUnk)
{
return E_POINTER;
}
HRESULT hr=E_FAIL;
IEnumFilters *pEnum=NULL;
IBaseFilter *pF=NULL;
if(FAILED(pGraph->EnumFilters(&pEnum)))
{
return E_FAIL;
}
//分别调用QueryInterface,枚举Filter Graph中的所有Filter
while(S_OK==pEnum->Next(1,&pF,0))
{
hr=pF->QueryInterface(iid,ppUnk);
pF->Release();
if(SUCCEEDED(hr))
{
break;
}
}
pEnum->Release();
return hr;
}
//查找给定Filter的Pin上实现的某个接口
HRESULT FindPinInterface(
IBaseFilter *pFilter, //Pointer to the filter to search
REFGUID iid, //IID of the interface
void **ppUnk) //Receives the interface pointer
{
if(!pFilter||!ppUnk)
{
return E_POINTER;
}
HRESULT hr=E_FAIL;
IEnumPins *pEnum=0;
if(FAILED(pFilter->EnumPins(&pEnum)))
{
return E_FAIL;
}
//分别调用QueryInterface,枚举Filter上的所有Pin
IPin *pPin=0;
while (S_OK==pEnum->Next(1,&pPin,0))
{
hr=pPin->QueryInterface(iid,ppUnk);
pPin->Release();
if(SUCCEEDED(hr))
{
break;
}
}
pEnum->Release();
return hr;
}
//综合了FindFilterInterface和FindPinInterface两个函数的功能
HRESULT FindInterfaceAnywhere(
IGraphBuilder *pGraph,
REFGUID iid,
void **ppUnk)
{
if(!pGraph||!ppUnk)
{
return E_POINTER;
}
HRESULT hr=E_FAIL;
IEnumFilters *pEnum=0;
if(FAILED(pGraph->EnumFilters(&pEnum)))
{
return E_FAIL;
}
//Loop through every filter in the graph
IBaseFilter *pF=0;
while(S_OK==pEnum->Next(1,&pF,0))
{
hr=pF->QueryInterface(iid,ppUnk);
if(FAILED(hr))
{
//The filter does not expose the interface, but maybe
//one of its pins does
hr=FindPinIterface(pF,iid,ppUnk);
}
pF->Release();
if(SUCCEEDED(hr))
{
break;
}
}
pF->Release();
return hr;
}
比如我们现在通过IGraphBuilder::RenderFile函数,构建了一条内含DV格式数据的AVI文件的回放
链路。我们想要得到其中DV视频解码Filter上的IIPDVDec接口,以设置DV解码输出的图像大小为原始图
像的四分之一。实现代码如下:
hr=pGraph->RenderFile(L"C:\\Example.avi",0);
if(SUCCEEDED(hr))
{
IIPDVDec *pDvDec;
hr=FindFilterInterface(pGraph,IID_IIPDVDec,(void **)&pDvDec);
if(SUCCEEDED(hr))
{
pDvDec->put_IPDisplay(DVRESOLUTION_QUARTER);
pDvDec->Release();
}
else if(hr==E_NOINTERFACE)
{
//This file does not contain DV video
}
else
{
//Some other error occurred
}
}
两个Filter的连接,总是从上一级Filter的输出Pin指向下一级Filter的输入Pin。
Filter Graph Manager提供的直接连接两个Filter的函数为IFilterGraph::ConnectDirect和IGraphBuilder::Connect。
参考代码如下:
HRESULT ConnectFilters(
IGraphBuilder *pGraph, //Filter Graph Manager
IPin *pOut, //Output pin on the upstream filter
IBaseFilter *pDest) //Downstream filter
{
if((pGraph==NULL)||(pOut==NULL)||(pDest==NULL))
{
return E_POINTER;
}
#ifdef debug
PIN_DIRECTION PinDir;
pOut->QueryDirection(&PinDir);
_ASSERTE(PinDir==PINDIR_OUTPUT);
#endif
//得到下一级Filter的输入Pin
IPin *pIn=0;
HRESULT hr=GetUnconnectedPin(pDest,PINDIR_INPUT,&pIn);
if(FAILED(hr))
{
return hr;
}
//将输出Pin连接到输入Pin
hr=pGraph->Connect(pOut,pIn);
pIn->Release();
return hr;
}
//不同参数的ConnectFilters函数重载形式
HRESULT ConnectFilters(
IGraphBuilder *pGraph,
IBaseFilter *pSrc,
IBaseFilter *pDest)
{
if((pGraph==NULL)||(pSrc==NULL)||(pDest==NULL))
{
return E_POINTER;
}
//Find an output pin on the first filter
IPin *pOut=0;
HRESULT hr=GetUnconnectedPin(pSrc,PINDIR_OUTPUT,&pOut);
if(FAILED(hr))
{
return hr;
}
hr=ConnectFilters(pGraph,pOut,pDest);
pOut->Release();
return hr;
}
比如我们现在要加入一个AVI Mux Filter和一个File Writer Filter,然后将它们相连起来。
代码如下:
IBaseFilter *pMux,*pWrite;
hr=AddFilterByCLSID(pGraph,CLSID_AviDest,L"AVI Mux",&pMux);
if(SUCCEEDED(hr))
{
hr=AddFilterByCLSID(pGraph,CLSID_FileWriter,L"File Writer",&pWrite);
if(SUCCEEDED(hr))
{
hr=ConnectFilters(pGraph,pMux,pWrite);
/*Use IFileSinkFilter to set the file name (not shown).*/
pWrite->Release();
}
pMux->Release();
}
4.查找Filter或Pin上的接口
//查找Filter上实现的某个接口
HRESULT FindFilterInterface(
IGraphBuilder *pGraph, //Pointer to the Filter Graph Manager
REFGUID iid, //IID of the interface to retrieve
void **ppUnk) //Receives the interface pointer
{
if(!pGraph||!ppUnk)
{
return E_POINTER;
}
HRESULT hr=E_FAIL;
IEnumFilters *pEnum=NULL;
IBaseFilter *pF=NULL;
if(FAILED(pGraph->EnumFilters(&pEnum)))
{
return E_FAIL;
}
//分别调用QueryInterface,枚举Filter Graph中的所有Filter
while(S_OK==pEnum->Next(1,&pF,0))
{
hr=pF->QueryInterface(iid,ppUnk);
pF->Release();
if(SUCCEEDED(hr))
{
break;
}
}
pEnum->Release();
return hr;
}
//查找给定Filter的Pin上实现的某个接口
HRESULT FindPinInterface(
IBaseFilter *pFilter, //Pointer to the filter to search
REFGUID iid, //IID of the interface
void **ppUnk) //Receives the interface pointer
{
if(!pFilter||!ppUnk)
{
return E_POINTER;
}
HRESULT hr=E_FAIL;
IEnumPins *pEnum=0;
if(FAILED(pFilter->EnumPins(&pEnum)))
{
return E_FAIL;
}
//分别调用QueryInterface,枚举Filter上的所有Pin
IPin *pPin=0;
while (S_OK==pEnum->Next(1,&pPin,0))
{
hr=pPin->QueryInterface(iid,ppUnk);
pPin->Release();
if(SUCCEEDED(hr))
{
break;
}
}
pEnum->Release();
return hr;
}
//综合了FindFilterInterface和FindPinInterface两个函数的功能
HRESULT FindInterfaceAnywhere(
IGraphBuilder *pGraph,
REFGUID iid,
void **ppUnk)
{
if(!pGraph||!ppUnk)
{
return E_POINTER;
}
HRESULT hr=E_FAIL;
IEnumFilters *pEnum=0;
if(FAILED(pGraph->EnumFilters(&pEnum)))
{
return E_FAIL;
}
//Loop through every filter in the graph
IBaseFilter *pF=0;
while(S_OK==pEnum->Next(1,&pF,0))
{
hr=pF->QueryInterface(iid,ppUnk);
if(FAILED(hr))
{
//The filter does not expose the interface, but maybe
//one of its pins does
hr=FindPinIterface(pF,iid,ppUnk);
}
pF->Release();
if(SUCCEEDED(hr))
{
break;
}
}
pF->Release();
return hr;
}
比如我们现在通过IGraphBuilder::RenderFile函数,构建了一条内含DV格式数据的AVI文件的回放
链路。我们想要得到其中DV视频解码Filter上的IIPDVDec接口,以设置DV解码输出的图像大小为原始图
像的四分之一。实现代码如下:
hr=pGraph->RenderFile(L"C:\\Example.avi",0);
if(SUCCEEDED(hr))
{
IIPDVDec *pDvDec;
hr=FindFilterInterface(pGraph,IID_IIPDVDec,(void **)&pDvDec);
if(SUCCEEDED(hr))
{
pDvDec->put_IPDisplay(DVRESOLUTION_QUARTER);
pDvDec->Release();
}
else if(hr==E_NOINTERFACE)
{
//This file does not contain DV video
}
else
{
//Some other error occurred
}
}
5.遍历Filter链路
给定一条Filter链路中的某个Filter,我们可以往上或往下遍历得到所有的其他Filter。方法是:从
本级Filter的输入Pin或者输出Pin,可以得到上一级或下一级Filter的连接着的Pin,再调用这个Pin的
IPin::QueryPinInfo就可以得到它的拥有者Filter。重复上述过程,我们就可以一直遍历到Source Filter
或者Renderer Filter。
HRESULT GetNextFilter(
IBaseFilter *pFilter, //遍历开始的Filter
PIN_DIRECTION Dir, //遍历方向
IBaseFilter **ppNext) //得到的相邻Filter
{
if(!pFilter||!ppNext)
{
return E_POINTER;
}
IEnumPins *pEnum=0;
IPin *pPin=0;
HRESULT hr=pFilter->EnumPins(&pEnum); //枚举Filter上所有Pin
if(FAILED(hr))
{
return hr;
}
while(S_OK==pEnum->Next(1,&pPin,0))
{
//See if this pin matches the specified direction
PIN_DIRECTION ThisPinDir;
hr=pPin->QueryDirection(&ThisPinDir);
if(FAILED(hr))
{
//Something strange happened.
hr=E_UNEXPECTED;
pPin->Release();
break;
}
if(ThisPinDir==Dir)
{
//Check if the pin is connected to another pin
IPin *pPinNext=0;
hr=pPin->ConnectedTo(&pPinNext);
if(SUCCEEDED(hr))
{
//Get the filter that owns that pin
PIN_INFO PinInfo;
hr=pPinNext->QueryPinInfo(&PinInfo);
pPinNext->Release();
pPin->Release();
pEnum->Release();
if(FAILED(hr)||(PinInfo.pFilter==NULL))
{
//Something strange happened.
return E_UNEXPECTED;
}
//This is the filter we're looking for
*ppNext=PinInfo.pFilter; //Client must release
return S_OK;
}
}
pPin->Release();
}
pEnum->Release();
}
6.成批删除Filter
//首先停止Filter Graph的运行
pControl->Stop();
//枚举Filter Graph中的所有Filter
IEnumFilters *pEnum=NULL;
HRESULT hr=pGraph->EnumFilters(&pEnum);
if(SUCCEEDED(hr))
{
IBaseFilter *pFilter=NULL;
while(S_OK==pEnum->Next(1,&pFilter,NULL))
{
pGraph->RemoveFilter(pFilter); //删除Filter
pEnum->Reset(); //复位枚举器内部状态
pFilter->Release();
}
pEnum->Release();
}
//递归调用,删除指定Filter下游的(连接着的)所有Filter的代码如下:
void NukeDownstream(IGraphBuilder *inGraph,IBaseFilter *inFilter)
{
if(inGraph&&inFilter)
{
IEnumPins *pinEnum=0;
if(SUCCEEDED(inFilter->EnumPins(&pinEnum)))
{
pinEnum->Reset();
IPin *pin=0;
ULONG cFetched=0;
bool pass=true;
//枚举Filter上的所有Pin
while(pass&&SUCCEEDED(pinEnum->Next(1,&pin,&cFetched)))
{
if(pin&&cFetched)
{
IPin *connectedPin=0;
//判断当前Pin是否处于连接状态
pin->ConnectedTo(&connectedPin);
if(connectedPin)
{
PIN_INFO pininfo;
if(SUCCEEDED(connectedPin->QueryPinInfo(&pininfo)))
{
//如果当前Pin连接着的Pin是输入Pin,则说明我们能够得到
//一个下一级Filter,我们要递归调用NukeDownstream
if(pininfo.dir==PINDIR_INPUT)
{
//这里递归调用
NukeDownstream(inGraph,pininfo.pFilter);
//当这一路下游Filter全部删除后,可以断开当前Pin连接
inGraph->Disconnect(connectedPin);
inGraph->Disconnect(pin);
//将操作完成的下一级Filter删除
inGraph->RemoveFilter(pininfo.pFilter);
}
pininfo.pFilter->Release();
}
connectedPin->Release();
}
pin->Release();
}
else
{
pass=false;
}
}
pinEnum->Release();
}
}
}
递归调用,删除指定Filter上游的(连接着的)所有Filter的代码如下:
void NukeUpstream(IGraphBuilder *inGraph,IBaseFilter *inFilter)
{
if(inGraph&&inFilter)
{
IEnumPins *pinEnum=0;
if(SUCCEEDED(inFilter->EnumPins(&pinEnum)))
{
pinEnum->Reset();
IPin *pin=0;
ULONG cFetched=0;
bool pass=true;
//枚举Filter上的所有Pin
while(pass&&SUCCEEDED(pinEnum->Next(1,&pin,&cFetched)))
{
if(pin&&cFetched)
{
IPin *connectedPin=0;
//判断当前Pin是否处于连接状态
pin->ConnectedTo(&connectedPin);
if(connectedPin)
{
PIN_INFO pininfo;
if(SUCCEEDED(connectedPin->QueryPinInfo(&pininfo)))
{
//如果当前Pin连接着的Pin是输出Pin,则说明我们能够得到
//一个上一级Filter,我们要递归调用NukeUpstream
if(pininfo.dir==PINDIR_OUTPUT)
{
//递归调用
NukeUpstream(inGraph,pininfo.pFilter);
//当这一路上游Filter全部删除后,可以断开当前Pin连接
inGraph->Disconnect(connectedPin);
inGraph->Disconnect(pin);
//将操作完成的上一级Filter删除
inGraph->RemoveFilter(pininfo.pFilter);
}
pininfo.pFilter->Release();
}
connectedPin->Release();
}
pin->Release();
}
else
{
pass=false;
}
}
pinEnum->Release();
}
}
}
事件交互的实现
事件通知(Event Notification)能够让应用程序与Filter Graph Manager之间实现交互控制。DirectShow中
定义的常见的事件有:EC_COMPLETE,表示Filter Graph中所有的数据都已经回放完毕(此时Filter Graph 不会自动
转入停止状态,需要在应用程序接收到这个事件后调用Filter Graph Manager的IMediaControl::Stop);
EC_ERRORABORT,表示Filter Graph运行时出错;EC_DEVICE_LOST,表示热插拔设备(典型的如USB设备、1394接口
设备等)脱离系统:EC_REPAINT,表示视频窗口要求重画当前图像帧。
Filter Graph Manager上有3个接口与事件通知有关,即IMediaEventSink、IMediaEvent和IMediaEventEx。
IMediaEventSink:用在Filter内部,它的接口方法Notify用于向Filter Graph Manager发送事件通知;
IMediaEvent:应用程序就是使用它的接口方法来处理Filter Graph Manager发出的事件;
IMediaEventEx:IMediaEvent接口的扩展,支持当Filter Graph Manager有事件要发出时,用窗口消息的方式通知
应用程序。
事件处理的大致过程如下:
(1)Filter Graph中的Filter发出一个事件,接收者为Filter Graph Manager;
(2)Filter Graph Manager对一些特殊的事件有默认的处理方法。在接收到事件后,要么按默认方法直接处理这个
事件,要么放到一个事件队列中(我们当然也可以通过调用接口方法IMediaEvent::CancelDefaultHandling来取消指定
事件的默认处理,使Filter Graph Manager接收到指定的事件后,直接将其放到事件队列中),等待上层的应用程序处
理;
(3)应用程序在获知Filter Graph Manager有事件发出后,就可以使用IMediaEventEx接口方法从事件队列中读取
事件,然后根据事件类型作出相应的处理。
应用程序对事件通知的处理的两种方法。
1.Filter Graph Manager通过发送指定的窗口消息通知应用程序
(1)自定义一个消息,然后调用IMediaEventEx::SetNotifyWindow将其设置给Filter Graph Manager,代码如下:
#define WM_GRAPHNOTIFY (WM_APP+100) //Private message.
//...
IMediaEventEx *pEvent=NULL;
hr=pGraph->QueryInterface(IID_IMediaEventEx,(void **)&pEvent);
hr=pEvent->SetNotifyWindow((OAHWND)m_hwnd,WM_GRAPHNOTIFY,0);
(2)在指定的消息接收窗口类中定义消息映射,代码如下:
BEGIN_MESSAGE_MAP(CNotifyWnd,...)
//{{AFX_MSG_MAP(CNotifyWnd)
//}}AFX_MSG_MAP
ON_MESSAGE(WM_GRAPHNOTIFY,OnGraphNotify)
END_MESSAGE_MAP()
(3)在消息响应函数中获取Filter Graph的事件通知,并作出相应处理,代码如下:
void CNotifyWnd::OnGraphNotify(WPARAM inWParam,LPARAM inLParam)
{
if(pEvent)
{
LONG eventCode=0,eventParam1=0,eventParam2=0;
while(SUCCEEDED(pEvent->GetEvent(&eventCode,&eventParam1,&eventParam2,0)))
{
pEvent->FreeEventParams(eventCode,eventParam1,eventParam2);
switch(eventCode)
{
case EC_COMPLETE:
//Do something...
break;
case EC_USERABORT:
//Do something...
break;
case EC_ERRORABORT:
//Do something...
break;
default:
break;
}
}
}
}
值得注意的是,由于Filter Graph Manager使用事件队列,与窗口消息的处理一样都是异步的。在得到事件后,应
调用IMediaEvent::FreeEventParams释放Filter Graph Manager为该事件分配的资源。
在释放IMediaEventEx接口之前,务必再次调用IMediaEventEx::SetNotifyWindow,将参数设置为NULL:
pEvent->SetNotifyWindow(NULL,0,0)。
2.使用事件同步对象:Filter Graph Manager在内部创建了一个事件同步对象,它在事件队列中尚未处理的事件时,
状态标记为有效,而当应用程序不断调用接口方法IMediaEvent::GetEvent取空事件队列中所有事件之后状态复位。我们
可以通过接口方法IMediaEvent::GetEventHandle来得到这个事件同步对象的句柄。代码如下:
HANDLE hEvent;
long evCode,param1,param2;
BOOLEAN bDone=FALSE;
HRESULT hr=S_OK;
hr=pEvent->GetEventHandle((OAEVENT*)&hEvent);
while(!bDone)
{
if(WAIT_OBJECT_0==WaitForSingleObject(hEvent,100))
{
while(hr=pEvent->GetEvent(&evCode,¶ml,¶m2,0),SUCCEEDED(hr))
{
printf("Event code: %#04x\n Params:%d, %d\n",evCode,param1,param2);
hr=pEvent->FreeEventParams(evCode,param1,param2);
bDone=(EC_COMPLETE==evCode);
}
}
}
进度条的实现
用VC的CSliderCtrl控件做进度条和定时器不断获取当前的播放时间点调整进度条的位置,实现播放进度即时显示。
实现随机位置播放,拖动进度条,获取进度值,然后转化为Filter Graph的播放进度时间点,并调用接口方法
IMediaSeeking::SetPositions进行设置。
给定一条Filter链路中的某个Filter,我们可以往上或往下遍历得到所有的其他Filter。方法是:从
本级Filter的输入Pin或者输出Pin,可以得到上一级或下一级Filter的连接着的Pin,再调用这个Pin的
IPin::QueryPinInfo就可以得到它的拥有者Filter。重复上述过程,我们就可以一直遍历到Source Filter
或者Renderer Filter。
HRESULT GetNextFilter(
IBaseFilter *pFilter, //遍历开始的Filter
PIN_DIRECTION Dir, //遍历方向
IBaseFilter **ppNext) //得到的相邻Filter
{
if(!pFilter||!ppNext)
{
return E_POINTER;
}
IEnumPins *pEnum=0;
IPin *pPin=0;
HRESULT hr=pFilter->EnumPins(&pEnum); //枚举Filter上所有Pin
if(FAILED(hr))
{
return hr;
}
while(S_OK==pEnum->Next(1,&pPin,0))
{
//See if this pin matches the specified direction
PIN_DIRECTION ThisPinDir;
hr=pPin->QueryDirection(&ThisPinDir);
if(FAILED(hr))
{
//Something strange happened.
hr=E_UNEXPECTED;
pPin->Release();
break;
}
if(ThisPinDir==Dir)
{
//Check if the pin is connected to another pin
IPin *pPinNext=0;
hr=pPin->ConnectedTo(&pPinNext);
if(SUCCEEDED(hr))
{
//Get the filter that owns that pin
PIN_INFO PinInfo;
hr=pPinNext->QueryPinInfo(&PinInfo);
pPinNext->Release();
pPin->Release();
pEnum->Release();
if(FAILED(hr)||(PinInfo.pFilter==NULL))
{
//Something strange happened.
return E_UNEXPECTED;
}
//This is the filter we're looking for
*ppNext=PinInfo.pFilter; //Client must release
return S_OK;
}
}
pPin->Release();
}
pEnum->Release();
}
6.成批删除Filter
//首先停止Filter Graph的运行
pControl->Stop();
//枚举Filter Graph中的所有Filter
IEnumFilters *pEnum=NULL;
HRESULT hr=pGraph->EnumFilters(&pEnum);
if(SUCCEEDED(hr))
{
IBaseFilter *pFilter=NULL;
while(S_OK==pEnum->Next(1,&pFilter,NULL))
{
pGraph->RemoveFilter(pFilter); //删除Filter
pEnum->Reset(); //复位枚举器内部状态
pFilter->Release();
}
pEnum->Release();
}
//递归调用,删除指定Filter下游的(连接着的)所有Filter的代码如下:
void NukeDownstream(IGraphBuilder *inGraph,IBaseFilter *inFilter)
{
if(inGraph&&inFilter)
{
IEnumPins *pinEnum=0;
if(SUCCEEDED(inFilter->EnumPins(&pinEnum)))
{
pinEnum->Reset();
IPin *pin=0;
ULONG cFetched=0;
bool pass=true;
//枚举Filter上的所有Pin
while(pass&&SUCCEEDED(pinEnum->Next(1,&pin,&cFetched)))
{
if(pin&&cFetched)
{
IPin *connectedPin=0;
//判断当前Pin是否处于连接状态
pin->ConnectedTo(&connectedPin);
if(connectedPin)
{
PIN_INFO pininfo;
if(SUCCEEDED(connectedPin->QueryPinInfo(&pininfo)))
{
//如果当前Pin连接着的Pin是输入Pin,则说明我们能够得到
//一个下一级Filter,我们要递归调用NukeDownstream
if(pininfo.dir==PINDIR_INPUT)
{
//这里递归调用
NukeDownstream(inGraph,pininfo.pFilter);
//当这一路下游Filter全部删除后,可以断开当前Pin连接
inGraph->Disconnect(connectedPin);
inGraph->Disconnect(pin);
//将操作完成的下一级Filter删除
inGraph->RemoveFilter(pininfo.pFilter);
}
pininfo.pFilter->Release();
}
connectedPin->Release();
}
pin->Release();
}
else
{
pass=false;
}
}
pinEnum->Release();
}
}
}
递归调用,删除指定Filter上游的(连接着的)所有Filter的代码如下:
void NukeUpstream(IGraphBuilder *inGraph,IBaseFilter *inFilter)
{
if(inGraph&&inFilter)
{
IEnumPins *pinEnum=0;
if(SUCCEEDED(inFilter->EnumPins(&pinEnum)))
{
pinEnum->Reset();
IPin *pin=0;
ULONG cFetched=0;
bool pass=true;
//枚举Filter上的所有Pin
while(pass&&SUCCEEDED(pinEnum->Next(1,&pin,&cFetched)))
{
if(pin&&cFetched)
{
IPin *connectedPin=0;
//判断当前Pin是否处于连接状态
pin->ConnectedTo(&connectedPin);
if(connectedPin)
{
PIN_INFO pininfo;
if(SUCCEEDED(connectedPin->QueryPinInfo(&pininfo)))
{
//如果当前Pin连接着的Pin是输出Pin,则说明我们能够得到
//一个上一级Filter,我们要递归调用NukeUpstream
if(pininfo.dir==PINDIR_OUTPUT)
{
//递归调用
NukeUpstream(inGraph,pininfo.pFilter);
//当这一路上游Filter全部删除后,可以断开当前Pin连接
inGraph->Disconnect(connectedPin);
inGraph->Disconnect(pin);
//将操作完成的上一级Filter删除
inGraph->RemoveFilter(pininfo.pFilter);
}
pininfo.pFilter->Release();
}
connectedPin->Release();
}
pin->Release();
}
else
{
pass=false;
}
}
pinEnum->Release();
}
}
}
事件交互的实现
事件通知(Event Notification)能够让应用程序与Filter Graph Manager之间实现交互控制。DirectShow中
定义的常见的事件有:EC_COMPLETE,表示Filter Graph中所有的数据都已经回放完毕(此时Filter Graph 不会自动
转入停止状态,需要在应用程序接收到这个事件后调用Filter Graph Manager的IMediaControl::Stop);
EC_ERRORABORT,表示Filter Graph运行时出错;EC_DEVICE_LOST,表示热插拔设备(典型的如USB设备、1394接口
设备等)脱离系统:EC_REPAINT,表示视频窗口要求重画当前图像帧。
Filter Graph Manager上有3个接口与事件通知有关,即IMediaEventSink、IMediaEvent和IMediaEventEx。
IMediaEventSink:用在Filter内部,它的接口方法Notify用于向Filter Graph Manager发送事件通知;
IMediaEvent:应用程序就是使用它的接口方法来处理Filter Graph Manager发出的事件;
IMediaEventEx:IMediaEvent接口的扩展,支持当Filter Graph Manager有事件要发出时,用窗口消息的方式通知
应用程序。
事件处理的大致过程如下:
(1)Filter Graph中的Filter发出一个事件,接收者为Filter Graph Manager;
(2)Filter Graph Manager对一些特殊的事件有默认的处理方法。在接收到事件后,要么按默认方法直接处理这个
事件,要么放到一个事件队列中(我们当然也可以通过调用接口方法IMediaEvent::CancelDefaultHandling来取消指定
事件的默认处理,使Filter Graph Manager接收到指定的事件后,直接将其放到事件队列中),等待上层的应用程序处
理;
(3)应用程序在获知Filter Graph Manager有事件发出后,就可以使用IMediaEventEx接口方法从事件队列中读取
事件,然后根据事件类型作出相应的处理。
应用程序对事件通知的处理的两种方法。
1.Filter Graph Manager通过发送指定的窗口消息通知应用程序
(1)自定义一个消息,然后调用IMediaEventEx::SetNotifyWindow将其设置给Filter Graph Manager,代码如下:
#define WM_GRAPHNOTIFY (WM_APP+100) //Private message.
//...
IMediaEventEx *pEvent=NULL;
hr=pGraph->QueryInterface(IID_IMediaEventEx,(void **)&pEvent);
hr=pEvent->SetNotifyWindow((OAHWND)m_hwnd,WM_GRAPHNOTIFY,0);
(2)在指定的消息接收窗口类中定义消息映射,代码如下:
BEGIN_MESSAGE_MAP(CNotifyWnd,...)
//{{AFX_MSG_MAP(CNotifyWnd)
//}}AFX_MSG_MAP
ON_MESSAGE(WM_GRAPHNOTIFY,OnGraphNotify)
END_MESSAGE_MAP()
(3)在消息响应函数中获取Filter Graph的事件通知,并作出相应处理,代码如下:
void CNotifyWnd::OnGraphNotify(WPARAM inWParam,LPARAM inLParam)
{
if(pEvent)
{
LONG eventCode=0,eventParam1=0,eventParam2=0;
while(SUCCEEDED(pEvent->GetEvent(&eventCode,&eventParam1,&eventParam2,0)))
{
pEvent->FreeEventParams(eventCode,eventParam1,eventParam2);
switch(eventCode)
{
case EC_COMPLETE:
//Do something...
break;
case EC_USERABORT:
//Do something...
break;
case EC_ERRORABORT:
//Do something...
break;
default:
break;
}
}
}
}
值得注意的是,由于Filter Graph Manager使用事件队列,与窗口消息的处理一样都是异步的。在得到事件后,应
调用IMediaEvent::FreeEventParams释放Filter Graph Manager为该事件分配的资源。
在释放IMediaEventEx接口之前,务必再次调用IMediaEventEx::SetNotifyWindow,将参数设置为NULL:
pEvent->SetNotifyWindow(NULL,0,0)。
2.使用事件同步对象:Filter Graph Manager在内部创建了一个事件同步对象,它在事件队列中尚未处理的事件时,
状态标记为有效,而当应用程序不断调用接口方法IMediaEvent::GetEvent取空事件队列中所有事件之后状态复位。我们
可以通过接口方法IMediaEvent::GetEventHandle来得到这个事件同步对象的句柄。代码如下:
HANDLE hEvent;
long evCode,param1,param2;
BOOLEAN bDone=FALSE;
HRESULT hr=S_OK;
hr=pEvent->GetEventHandle((OAEVENT*)&hEvent);
while(!bDone)
{
if(WAIT_OBJECT_0==WaitForSingleObject(hEvent,100))
{
while(hr=pEvent->GetEvent(&evCode,¶ml,¶m2,0),SUCCEEDED(hr))
{
printf("Event code: %#04x\n Params:%d, %d\n",evCode,param1,param2);
hr=pEvent->FreeEventParams(evCode,param1,param2);
bDone=(EC_COMPLETE==evCode);
}
}
}
进度条的实现
用VC的CSliderCtrl控件做进度条和定时器不断获取当前的播放时间点调整进度条的位置,实现播放进度即时显示。
实现随机位置播放,拖动进度条,获取进度值,然后转化为Filter Graph的播放进度时间点,并调用接口方法
IMediaSeeking::SetPositions进行设置。
Filter属性页的显示
使用OleCreatePropertyFrame函数,在应用程序中直接显示Filter的属性页的方法,代码如下:
IBaseFilter *pFilter;
/*Obtain the filter's IBaseFilter interface.(Not shown)*/
ISpecifyPropertyPages *pProp=NULL;
HRESULT hr=pFilter->QueryInterface(IID_ISpecifyPropertyPages,(void **)&pProp);
if(SUCCEEDED(hr))
{
//Get the filter's name and IUnknown pointer.
FILTER_INFO FilterInfo;
hr=pFilter->QueryFilterInfo(&FilterInfo);
IUnknown *pFilterUnk;
pFilter->QueryInterface(IID_IUnknown,(void **)&pFilterUnk);
//Show the page
CAUUID caGUID;
pProp->GetPages(&caGUID);
pProp->Release();
OleCreatePropertyFrame(
hWnd, //Parent window
0,0, //Reserved
FilterInfo.achName, //Caption for the dialog box
1, //Number of objects(just the filter)
&pFilterUnk, //Array of object pointers.
caGUID.cElems, //Number of property pages
caGUID.pElems, //Array of property page CLSIDs
0, //Locale identifier
0,NULL //Reserved
);
//Clean up
pFilterUnk->Release();
FilterInfo.pGraph->Release();
CoTaskMemFree(caGUID.pElems);
}
系统设备的枚举
系统枚举过程:
(1)使用CoCreateInstance函数创建系统枚举器组件对象(CLSID为CLSID_SystemDeviceEnum),并获得ICreateDevEnum接口;
(2)使用接口方法ICreateDevEnum::CreateClassEnumerator为指定的Filter注册类型目录创建一个枚举器,并获得
IEnumMoniker接口;
(3)使用IEnumMoniker接口方法枚举指定类型目录下所有设备标识(Device Moniker);
(4)调用IMoniker::BindToStorage之后,可以访问设备标识的属性集,比如得到Display Name、Friendly Name等;
(5)调用IMoniker::BindToObject可以将设备标识生成一个DirectShow Filter,随后调用IFilterGraph::AddFilter,并将之
加入到Filter Graph中就可以参与工作了。
代码如下:
//创建一个系统枚举组件
HRESULT hr;
ICreateDevEnum *pSysDevEnum=NULL;
hr=CoCreateInstance(CLSID_SystemDeviceEnum,NULL,CLSCTX_INPROC_SERVER,IID_ICreateDecEnum,(void **)&pSysDevEnum);
if(FAILED(hr))
{
return hr;
}
//指定枚举的类型目录,获取IEnumMoniker接口
IEnumMoniker *pEnumCat=NULL;
hr=pSysDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,
&pEnumCat,0);
if(hr==S_OK)
{
//使用IEnumMoniker接口枚举所有的设备标识
IMoniker *pMoniker=NULL;
ULONG cFetched;
while(pEnumCat->Next(1,&pMoniker,&cFetched)==S_OK)
{
IPropertBag *pPropBag;
hr=pMoniker->BindToStorage(0,0,IID_IPropertyBag,(void **)&pPropBag);
if(SUCCEEDED(hr))
{
//获取Filter的Friendly Name
VARIANT varName;
VariantInit(&varName);
hr=pPropBag->Read(L"FriendlyName",&varName,0);
if(SUCCEEDED(hr))
{
//Display the name in your UI somehow
}
VariantClear(&varName);
//创建Filter实例
IBaseFilter *pFilter;
hr=pMoniker->BindToObject(NULL,NULL,IID_IBaseFilter,(void**)&pFilter);
//Now add the filter to the graph
//Remember to release pFilter later
pPropBag->Release();
}
pMoniker->Release();
}
pEnumCat->Release();
}
pSysDevEnum->Release();
知道了一个硬件的设备标识,我们也可以直接通过IFilterGraph2::AddSourceFilterForMoniker接口方法来创建
Filter,代码如下:
LPOLESTR strName=NULL;
IBaseFilter pSrc=NULL;
hr=pMoniker->GetDisplayName(NULL,NULL,&strName);
if(SUCCEEDED(hr))
{
//获取IFilterGraph2接口
IFilterGraph2 *pFG2=NULL;
hr=pGraph->QueryInterface(IID_IFilterGraph2,(void**)&pFG2);
if(SUCCEEDED(hr))
{
//根据设备标识创建一个Source Filter
hr=pFG2->AddSourceFilterForMoniker(pMoniker,0,L"Source",&pSrc);
pFG2->Release();
}
CoTaskMemFree(strName);
}
//If successful,remember to release pSrc
或者给定一个Filter类型目录的Display Name描述,格式为@device:*:{categoryclsid},调用IMoniker::ParseDisplayName
或MkParseDisplayName函数对字符串进行分析,创建一个该类型目录下第一个返回的设备标识对象。代码如下:
//视频采集设备的类型目录
WCHAR szMon[]=L"device:*:{860B310-5D01-11d0-BD3B-00A0C911CE86}";
IBindCtx *pBindCtx;
hr=CreateBindCtx(0,&pBindCtx);
ULONG chEaten=0;
IMoniker *pMoniker=0;
hr=MkParseDisplayName(pBindCtx,szMon,&chEaten,&pMoniker);
pBindCtx->Release();
if(SUCCEEDED(hr))
{
//Get the display name, or bind to a DirectShow filter
//...
pMoniker->Release();
}
图片的抓取
在播放媒体文件的过程中,有一个很有用的功能,就是在当前播放的位置抓取图片。实现这种图片抓取功能的方法很多,我们这里只介绍
常用的两种。
第一种,它使用IBasicVideo *pBasicVideo::GetCurrentImage接口方法,代码如下:
bool SnapshotBitmap(IBasicVideo *pBasicVideo,const char *outFile)
{
if(pBasicVideo)
{
long bitmapSize=0;
//首先获得图像大小
if(SUCCEEDED(pBasicVideo->GetCurrentImage(&bitmapSize,0)))
{
bool pass=false;
//分配图像帧内存
unsigned char *buffer=new unsigned char[bitmapSize];
//获取图像帧数据
if(SUCCEEDED(pBasicVideo->GetCurrentImage(&bitmapSize,(long *)buffer)))
{
BITMAPFILEHEADER hdr;
LPBITMAPINFOHEADER lpbi;
lpbi=(LPBITMAPINFOHEADER)buffer;
int nColors=1<<lpbi->biBitCount;
if(nColors>256)
{
nColors=0;
}
//always is "BM"
hdr.bfType =((WORD)('M'<<8)|'B');
hdr.bfSize =bitmapSize+sizeof(hdr);
hdr.bfReserved1 =0;
hdr.bfReserved2 =0;
hdr.bfOffBits =(DWORD)(sizeof(BITMAPFILEHEADER)+lpbi->biSize+nColors*sizeof(RGBQUAD));
CFile bitmapFile(outFile,CFile::modeReadWrite|CFile::modeCreate|CFile::typeBinary);
//写入位图文件头
bitmapFile.Write(&hdr,sizeof(BITMAPFILEHEADER));
//写入图像帧数据(包括BITMAPINFOHEADER信息)
bitmapFile.Write(buffer,bitmapSize);
bitmapFile.Close();
pass=true;
}
delete [] buffer;
return pass;
}
}
return false;
}
值得注意的是,IBaseFilter接口应该从Filter Graph Manager上获得,但真正实现在Renderer Filter上。如果我们使用的是传统的Video Renderer,
那么使用GetCurrentImage抓图将是不可靠的。因为如果Video Renderer使用了DirectDraw加速,这个函数调用会失败;而且调用这个函数,Video Renderer
必须处于暂停状态。但如果我们使用的是VMR,则没有上述这些限制。
第二种,它使用Sample Grabber Filter。它其实是一个Trans-In-Place Filter,在SDK安装目录下的Samples\C++\DirectShow\Filters\Grabber提供了
源代码。实际上,Sample Grabber可以抓取任何类型的Sample。但在这里,我们只介绍使用它抓取视频帧的方法。步骤如下:
(1)创建Sample Grabber,并将之加入到Filter Graph中。
//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
}
hr=pGraph->AddFilter(pGrabberF,L"Sample Grabber");
if(FAILED(hr))
{
//Return an error
}
ISampleGrabber *pGrabber=NULL;
pGrabberF->QueryInterface(IID_ISampleGrabber,(void **)&pGrabber);
(2)给Sample Grabber设置Pin上连接用的媒体类型。
如果我们想抓取24位的RGB图片,如下设置媒体类型:
AM_MEDIA_TYPE mt;
ZeroMemory(&mt,sizeof(AM_MEDIA_TYPE));
mt.majortype=MEDIATYPE_Video;
mt.subtype=MEDIASUBTYPE_RGB24;
hr=pGrabber->SetMediaType(&mt);
也可以根据当前显示器的配置来设置Sample Grabber接受的RGB类型,代码如下:
//Find the current bit depth
HDC hdc=GetDC(NULL);
int iBitDepth=GetDeviceCaps(hdc,BITSPIXEL);
ReleaseDC(NULL,hdc);
//Set the media type
mt.majortype=MEDIATYPE_Video;
switch(iBitDepth)
{
case 8:
mt.subtype=MEDIASUBTYPE_RGB8;
break;
case 16:
mt.subtype=MEDIASUBTYPE_RGB555;
break;
case 24:
mt.subtype=MEDIASUBTYPE_RGB24;
break;
case 32:
mt.subtype=MEDIASUBTYPE_RGB32;
break;
default:
return E_FAIL;
}
hr=pGrabber->SetMediaType(&mt);
(3)完成Filter Graph的构建。
因为Sample Grabber上已经设置了一个媒体类型,则其他Filter必须以这种媒体类型才能与Sample Grabber相连。我
们可以使用DrectShow的“智能连接”机制,来简化这个Filter Graph的创建过程,代码如下:
IBaseFilter *pSrc;
hr=pGraph->AddSourceFilter(wszFileName,L"Source",&pSrc);
if(FAILED(hr))
{
//Return an error code
}
hr=ConnectFilters(pGraph,pSrc,pGrabberF);
其中,ConnectFilters是我们在P151中介绍的自定义函数。
如果我们只想抓图(不需要对视频预览),则Sample Grabber后面可以连接一个Null Renderer Filter(它的CLSID为
CLSID_NullRenderer)。如果要Filter Graph中的数据流以最快的速度传送,则Filter Graph不要使用参考时钟(调用
IMediaFilter::SetSyncSource,参数为NULL)。
我们不推荐Sample Grabber后面连接标准的Video Renderer。因为Sample Grabber是一个Trans-In-Place Filter,输
入和输出Pin使用相同的数据缓存,而且这个内存很可能位于显卡上。而从显存中读取数据比直接从主机内存中读取数据要
慢得多,这将大大降低视频图像显示的性能。
(4)运行Filter Graph。
Sample Grabber可以有如下两种工作模式:
缓冲模式 将输入的Sample进行缓存后,再往下传送。
回调模式 当有输入的Sample时,调用应用程序设置进来的回调函数。
因为回调模式会影响整个Filter Graph的效率,并且容易引起死锁,所以我们推荐使用缓冲模式。另外,我们可以设置
ISampleGrabber::SetOneShot,使得Sample Grabber获取一个Sample以后,就让Filter Graph停止,代码如下:
//Set one-shot mode and buffering.
hr=pGrabber->SetOneShot(TRUE);
hr=pGrabber->SetBufferSamples(TRUE);
pControl->Run(); //Run the graph.
pEvent->WaitForCompletion(INFINITE,&evCode); //Wait till it's done.
(5)获取抓到的Sample数据。缓冲模式下,我们可以调用ISampleGrabber::GetCurrentBuffer来获取Sample数据,代码
如下:
//Find the required buffer size
long cbBuffer=0;
hr=pGrabber->GetCurrentBuffer(&cbBuffer,NULL);
char *pBuffer=new char[cbBuffer];
if(!pBuffer)
{
//Out of memory.Return an error code
}
hr=pGrabber->GetCurrentBuffer(&cbBuffer,(long*)pBuffer);
我们也可以将获取的数据使用GDI函数显示出来,代码如下:
AM_MEDIA_TYPE mt;
hr=pGrabber->GetConnectedMediaType(&mt);
if(FAILED(hr))
{
//Return error code
}
//Examine the format block
VIDEOINFOHEADER *pVih;
if((mt.formattype==FORMAT_VideoInfo)&&
(mt.cbFormat>=sizeof(VIDEOINFOHEADER))&&
(mt.pbFormat!=NULL))
{
pVih=(VIDEOINFOHEADER*)mt.pbFormat;
}
else
{
//Wrong format.Free the format block and return an error.
FreeMediaType(mt);
return VFW_E_INVALIDMEDIATYPE;
}
//You can use the media type to access the BITMAPINFOHEADRE information.
//For example,the following code draws the bitmap using GDI:
SetDIBitsToDevice(
hdc,0,0,
pVih->bmiHeader.biWidth,
pVih->bmiHeader.biHeight,
0,0,
0,
pVih->bmiHeader.biHeight,
pBuffer,
(BITMAPINFO*)&pVih->bmiHeader,
DIB_RGB_COLORS);
//Free the format block when you are done
FreeMediaType(mt);
使用OleCreatePropertyFrame函数,在应用程序中直接显示Filter的属性页的方法,代码如下:
IBaseFilter *pFilter;
/*Obtain the filter's IBaseFilter interface.(Not shown)*/
ISpecifyPropertyPages *pProp=NULL;
HRESULT hr=pFilter->QueryInterface(IID_ISpecifyPropertyPages,(void **)&pProp);
if(SUCCEEDED(hr))
{
//Get the filter's name and IUnknown pointer.
FILTER_INFO FilterInfo;
hr=pFilter->QueryFilterInfo(&FilterInfo);
IUnknown *pFilterUnk;
pFilter->QueryInterface(IID_IUnknown,(void **)&pFilterUnk);
//Show the page
CAUUID caGUID;
pProp->GetPages(&caGUID);
pProp->Release();
OleCreatePropertyFrame(
hWnd, //Parent window
0,0, //Reserved
FilterInfo.achName, //Caption for the dialog box
1, //Number of objects(just the filter)
&pFilterUnk, //Array of object pointers.
caGUID.cElems, //Number of property pages
caGUID.pElems, //Array of property page CLSIDs
0, //Locale identifier
0,NULL //Reserved
);
//Clean up
pFilterUnk->Release();
FilterInfo.pGraph->Release();
CoTaskMemFree(caGUID.pElems);
}
系统设备的枚举
系统枚举过程:
(1)使用CoCreateInstance函数创建系统枚举器组件对象(CLSID为CLSID_SystemDeviceEnum),并获得ICreateDevEnum接口;
(2)使用接口方法ICreateDevEnum::CreateClassEnumerator为指定的Filter注册类型目录创建一个枚举器,并获得
IEnumMoniker接口;
(3)使用IEnumMoniker接口方法枚举指定类型目录下所有设备标识(Device Moniker);
(4)调用IMoniker::BindToStorage之后,可以访问设备标识的属性集,比如得到Display Name、Friendly Name等;
(5)调用IMoniker::BindToObject可以将设备标识生成一个DirectShow Filter,随后调用IFilterGraph::AddFilter,并将之
加入到Filter Graph中就可以参与工作了。
代码如下:
//创建一个系统枚举组件
HRESULT hr;
ICreateDevEnum *pSysDevEnum=NULL;
hr=CoCreateInstance(CLSID_SystemDeviceEnum,NULL,CLSCTX_INPROC_SERVER,IID_ICreateDecEnum,(void **)&pSysDevEnum);
if(FAILED(hr))
{
return hr;
}
//指定枚举的类型目录,获取IEnumMoniker接口
IEnumMoniker *pEnumCat=NULL;
hr=pSysDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,
&pEnumCat,0);
if(hr==S_OK)
{
//使用IEnumMoniker接口枚举所有的设备标识
IMoniker *pMoniker=NULL;
ULONG cFetched;
while(pEnumCat->Next(1,&pMoniker,&cFetched)==S_OK)
{
IPropertBag *pPropBag;
hr=pMoniker->BindToStorage(0,0,IID_IPropertyBag,(void **)&pPropBag);
if(SUCCEEDED(hr))
{
//获取Filter的Friendly Name
VARIANT varName;
VariantInit(&varName);
hr=pPropBag->Read(L"FriendlyName",&varName,0);
if(SUCCEEDED(hr))
{
//Display the name in your UI somehow
}
VariantClear(&varName);
//创建Filter实例
IBaseFilter *pFilter;
hr=pMoniker->BindToObject(NULL,NULL,IID_IBaseFilter,(void**)&pFilter);
//Now add the filter to the graph
//Remember to release pFilter later
pPropBag->Release();
}
pMoniker->Release();
}
pEnumCat->Release();
}
pSysDevEnum->Release();
知道了一个硬件的设备标识,我们也可以直接通过IFilterGraph2::AddSourceFilterForMoniker接口方法来创建
Filter,代码如下:
LPOLESTR strName=NULL;
IBaseFilter pSrc=NULL;
hr=pMoniker->GetDisplayName(NULL,NULL,&strName);
if(SUCCEEDED(hr))
{
//获取IFilterGraph2接口
IFilterGraph2 *pFG2=NULL;
hr=pGraph->QueryInterface(IID_IFilterGraph2,(void**)&pFG2);
if(SUCCEEDED(hr))
{
//根据设备标识创建一个Source Filter
hr=pFG2->AddSourceFilterForMoniker(pMoniker,0,L"Source",&pSrc);
pFG2->Release();
}
CoTaskMemFree(strName);
}
//If successful,remember to release pSrc
或者给定一个Filter类型目录的Display Name描述,格式为@device:*:{categoryclsid},调用IMoniker::ParseDisplayName
或MkParseDisplayName函数对字符串进行分析,创建一个该类型目录下第一个返回的设备标识对象。代码如下:
//视频采集设备的类型目录
WCHAR szMon[]=L"device:*:{860B310-5D01-11d0-BD3B-00A0C911CE86}";
IBindCtx *pBindCtx;
hr=CreateBindCtx(0,&pBindCtx);
ULONG chEaten=0;
IMoniker *pMoniker=0;
hr=MkParseDisplayName(pBindCtx,szMon,&chEaten,&pMoniker);
pBindCtx->Release();
if(SUCCEEDED(hr))
{
//Get the display name, or bind to a DirectShow filter
//...
pMoniker->Release();
}
图片的抓取
在播放媒体文件的过程中,有一个很有用的功能,就是在当前播放的位置抓取图片。实现这种图片抓取功能的方法很多,我们这里只介绍
常用的两种。
第一种,它使用IBasicVideo *pBasicVideo::GetCurrentImage接口方法,代码如下:
bool SnapshotBitmap(IBasicVideo *pBasicVideo,const char *outFile)
{
if(pBasicVideo)
{
long bitmapSize=0;
//首先获得图像大小
if(SUCCEEDED(pBasicVideo->GetCurrentImage(&bitmapSize,0)))
{
bool pass=false;
//分配图像帧内存
unsigned char *buffer=new unsigned char[bitmapSize];
//获取图像帧数据
if(SUCCEEDED(pBasicVideo->GetCurrentImage(&bitmapSize,(long *)buffer)))
{
BITMAPFILEHEADER hdr;
LPBITMAPINFOHEADER lpbi;
lpbi=(LPBITMAPINFOHEADER)buffer;
int nColors=1<<lpbi->biBitCount;
if(nColors>256)
{
nColors=0;
}
//always is "BM"
hdr.bfType =((WORD)('M'<<8)|'B');
hdr.bfSize =bitmapSize+sizeof(hdr);
hdr.bfReserved1 =0;
hdr.bfReserved2 =0;
hdr.bfOffBits =(DWORD)(sizeof(BITMAPFILEHEADER)+lpbi->biSize+nColors*sizeof(RGBQUAD));
CFile bitmapFile(outFile,CFile::modeReadWrite|CFile::modeCreate|CFile::typeBinary);
//写入位图文件头
bitmapFile.Write(&hdr,sizeof(BITMAPFILEHEADER));
//写入图像帧数据(包括BITMAPINFOHEADER信息)
bitmapFile.Write(buffer,bitmapSize);
bitmapFile.Close();
pass=true;
}
delete [] buffer;
return pass;
}
}
return false;
}
值得注意的是,IBaseFilter接口应该从Filter Graph Manager上获得,但真正实现在Renderer Filter上。如果我们使用的是传统的Video Renderer,
那么使用GetCurrentImage抓图将是不可靠的。因为如果Video Renderer使用了DirectDraw加速,这个函数调用会失败;而且调用这个函数,Video Renderer
必须处于暂停状态。但如果我们使用的是VMR,则没有上述这些限制。
第二种,它使用Sample Grabber Filter。它其实是一个Trans-In-Place Filter,在SDK安装目录下的Samples\C++\DirectShow\Filters\Grabber提供了
源代码。实际上,Sample Grabber可以抓取任何类型的Sample。但在这里,我们只介绍使用它抓取视频帧的方法。步骤如下:
(1)创建Sample Grabber,并将之加入到Filter Graph中。
//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
}
hr=pGraph->AddFilter(pGrabberF,L"Sample Grabber");
if(FAILED(hr))
{
//Return an error
}
ISampleGrabber *pGrabber=NULL;
pGrabberF->QueryInterface(IID_ISampleGrabber,(void **)&pGrabber);
(2)给Sample Grabber设置Pin上连接用的媒体类型。
如果我们想抓取24位的RGB图片,如下设置媒体类型:
AM_MEDIA_TYPE mt;
ZeroMemory(&mt,sizeof(AM_MEDIA_TYPE));
mt.majortype=MEDIATYPE_Video;
mt.subtype=MEDIASUBTYPE_RGB24;
hr=pGrabber->SetMediaType(&mt);
也可以根据当前显示器的配置来设置Sample Grabber接受的RGB类型,代码如下:
//Find the current bit depth
HDC hdc=GetDC(NULL);
int iBitDepth=GetDeviceCaps(hdc,BITSPIXEL);
ReleaseDC(NULL,hdc);
//Set the media type
mt.majortype=MEDIATYPE_Video;
switch(iBitDepth)
{
case 8:
mt.subtype=MEDIASUBTYPE_RGB8;
break;
case 16:
mt.subtype=MEDIASUBTYPE_RGB555;
break;
case 24:
mt.subtype=MEDIASUBTYPE_RGB24;
break;
case 32:
mt.subtype=MEDIASUBTYPE_RGB32;
break;
default:
return E_FAIL;
}
hr=pGrabber->SetMediaType(&mt);
(3)完成Filter Graph的构建。
因为Sample Grabber上已经设置了一个媒体类型,则其他Filter必须以这种媒体类型才能与Sample Grabber相连。我
们可以使用DrectShow的“智能连接”机制,来简化这个Filter Graph的创建过程,代码如下:
IBaseFilter *pSrc;
hr=pGraph->AddSourceFilter(wszFileName,L"Source",&pSrc);
if(FAILED(hr))
{
//Return an error code
}
hr=ConnectFilters(pGraph,pSrc,pGrabberF);
其中,ConnectFilters是我们在P151中介绍的自定义函数。
如果我们只想抓图(不需要对视频预览),则Sample Grabber后面可以连接一个Null Renderer Filter(它的CLSID为
CLSID_NullRenderer)。如果要Filter Graph中的数据流以最快的速度传送,则Filter Graph不要使用参考时钟(调用
IMediaFilter::SetSyncSource,参数为NULL)。
我们不推荐Sample Grabber后面连接标准的Video Renderer。因为Sample Grabber是一个Trans-In-Place Filter,输
入和输出Pin使用相同的数据缓存,而且这个内存很可能位于显卡上。而从显存中读取数据比直接从主机内存中读取数据要
慢得多,这将大大降低视频图像显示的性能。
(4)运行Filter Graph。
Sample Grabber可以有如下两种工作模式:
缓冲模式 将输入的Sample进行缓存后,再往下传送。
回调模式 当有输入的Sample时,调用应用程序设置进来的回调函数。
因为回调模式会影响整个Filter Graph的效率,并且容易引起死锁,所以我们推荐使用缓冲模式。另外,我们可以设置
ISampleGrabber::SetOneShot,使得Sample Grabber获取一个Sample以后,就让Filter Graph停止,代码如下:
//Set one-shot mode and buffering.
hr=pGrabber->SetOneShot(TRUE);
hr=pGrabber->SetBufferSamples(TRUE);
pControl->Run(); //Run the graph.
pEvent->WaitForCompletion(INFINITE,&evCode); //Wait till it's done.
(5)获取抓到的Sample数据。缓冲模式下,我们可以调用ISampleGrabber::GetCurrentBuffer来获取Sample数据,代码
如下:
//Find the required buffer size
long cbBuffer=0;
hr=pGrabber->GetCurrentBuffer(&cbBuffer,NULL);
char *pBuffer=new char[cbBuffer];
if(!pBuffer)
{
//Out of memory.Return an error code
}
hr=pGrabber->GetCurrentBuffer(&cbBuffer,(long*)pBuffer);
我们也可以将获取的数据使用GDI函数显示出来,代码如下:
AM_MEDIA_TYPE mt;
hr=pGrabber->GetConnectedMediaType(&mt);
if(FAILED(hr))
{
//Return error code
}
//Examine the format block
VIDEOINFOHEADER *pVih;
if((mt.formattype==FORMAT_VideoInfo)&&
(mt.cbFormat>=sizeof(VIDEOINFOHEADER))&&
(mt.pbFormat!=NULL))
{
pVih=(VIDEOINFOHEADER*)mt.pbFormat;
}
else
{
//Wrong format.Free the format block and return an error.
FreeMediaType(mt);
return VFW_E_INVALIDMEDIATYPE;
}
//You can use the media type to access the BITMAPINFOHEADRE information.
//For example,the following code draws the bitmap using GDI:
SetDIBitsToDevice(
hdc,0,0,
pVih->bmiHeader.biWidth,
pVih->bmiHeader.biHeight,
0,0,
0,
pVih->bmiHeader.biHeight,
pBuffer,
(BITMAPINFO*)&pVih->bmiHeader,
DIB_RGB_COLORS);
//Free the format block when you are done
FreeMediaType(mt);