DirectShow用于视频音频播放,视频音频捕获,视频音频文件创作。还可以用于视频音频文件格式转换,编码方式转换等。
windows系统中带有过滤器图编辑器(graphedt.exe),使用它我们可以直观的构建过滤器图,进行运行和测试。
过滤器图编辑器在哪里?
它在Windows7系统中的位置是:
C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\graphedt.exe
C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\x64\graphedt.exe
它在Windows8系统中的位置是:
C:\Program Files (x86)\Windows Kits\8.1\bin\x86\graphedt.exe
C:\Program Files (x86)\Windows Kits\8.1\bin\x64\graphedt.exe
在Windows10系统中graphedt.exe所在的路径:
C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x86\graphedt.exe
C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\graphedt.exe
C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\arm64\graphedt.exe
第一个是32位,第二个是64位。
根据系统安装SDK版本的不同,可能在其它目录中还有graphedt.exe。
因为我们将编写32位DirectShow应用程序,所以选择32位GraphEdit,双击运行GraphEdit,GraphEdit界面如下:
点击“文件”(File)菜单,在下拉菜单中选择“渲染一个媒体文件”(Render Media File…);弹出“选择媒体文件对话框”,在对话框中选择并打开一个WAV音频文件。GraphEdit自动创建了一个过滤器图,界面图像如下:
GraphEdit客户区中的每个矩形窗口都统称为过滤器。
GraphEdit创建了3个过滤器:
第一个是文件源过滤器,它有一个输出引脚。
第二个是WAV解析过滤器,它有一个输入引脚和一个输出引脚。
第三个是默认DirectSound音频渲染过滤器,它只有一个输入引脚。
3个过滤器已自动连接。点击播放按钮,就可以播放该WAV音频文件。
使用同样方法,渲染一个avi视频文件,过滤器图如下:
当然我们也可以打开过滤器列表,手动添加过滤器,连接过滤器,然后运行。还可以保存当前过滤器图到grf文件。
查看过滤器属性
要查看过滤器属性,引脚属性;需在过滤器图编辑器所在目录下注册以下DLL:
proppage.dll
evrprop.dll
方法是:
在windows7系统打开“运行”窗口。
在“打开(O):”编辑框输入:
regsvr32.exe /s “C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\proppage.dll”
点击确定。
再次在在“打开(O):”编辑框输入:
regsvr32.exe /s “C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\evrprop.dll”
点击确定。
如果是windows8系统,则为:
regsvr32.exe /s “C:\Program Files (x86)\Windows Kits\8.1\bin\x86\proppage.dll”
regsvr32.exe /s “C:\Program Files (x86)\Windows Kits\8.1\bin\x86\evrprop.dll”
如果是windows10系统,则为:
regsvr32.exe /s “C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x86\proppage.dll”
regsvr32.exe /s “C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x86\evrprop.dll”
这样就可以查看过滤器属性和引脚属性了。在过滤器上点击右键,选择“过滤器属性”菜单项。
过滤器图的代码实现
以上面播放wav音频文件过滤器图为例,它的代码实现为:
IGraphBuilder* pGraph = NULL;
IMediaControl* pControl = NULL;
IBasicAudio* pIBasicAudio = NULL;
HRESULT hr;
IBaseFilter* pFileSourceAsync = NULL;
IFileSourceFilter* pIFileSourceFilter1 = NULL;
IPin* pFileSourceAsync_Output = NULL;
IBaseFilter* pWaveParser = NULL;
GUID WaveParser_guid = { 0xd51bd5a1, 0x7548, 0x11cf, 0xa5, 0x20, 0x00, 0x80, 0xc7, 0x7e, 0xf5, 0x8a };
IPin* pWaveParser_inputpin = NULL;
IPin* pWaveParser_output = NULL;
IBaseFilter* pDefaultDirectSoundDevice = NULL;
IPin* pDefaultDirectSoundDevice_AudioInputpinrendered = NULL;
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGraph));//创建过滤器图管理器
if (hr != S_OK)goto Exit;
hr = pGraph->QueryInterface(IID_IMediaControl, (void**)&pControl);//获取媒体控制接口
if (hr != S_OK)goto Exit;
hr = pGraph->QueryInterface(IID_IBasicAudio, (void**)&pIBasicAudio);//获取音量和平衡控制接口
if (hr != S_OK)goto Exit;
hr = CoCreateInstance(CLSID_AsyncReader, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (LPVOID*)&pFileSourceAsync);//创建FileSourceAsync过滤器
if (hr != S_OK)goto Exit;
hr = pGraph->AddFilter(pFileSourceAsync, L"FileSourceAsync");
if (hr != S_OK)goto Exit;
hr = pFileSourceAsync->QueryInterface(IID_PPV_ARGS(&pIFileSourceFilter1));//获取IFileSourceFilter接口
if (hr != S_OK)goto Exit;
if (pIFileSourceFilter1 != NULL)
{
hr = pIFileSourceFilter1->Load(L"D:\\音频视频\\音频\\不同编码方式的WAV\\PCM.wav", NULL);//加载媒体文件
if (hr != S_OK)goto Exit;
pIFileSourceFilter1->Release(); pIFileSourceFilter1 = NULL;
}
hr = pFileSourceAsync->FindPin(L"Output", &pFileSourceAsync_Output);
if (hr != S_OK)goto Exit;
hr = CoCreateInstance(WaveParser_guid, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (LPVOID*)&pWaveParser);//创建WaveParser过滤器
if (hr != S_OK)goto Exit;
hr = pGraph->AddFilter(pWaveParser, L"WaveParser");
if (hr != S_OK)goto Exit;
hr = pWaveParser->FindPin(L"input pin", &pWaveParser_inputpin);
if (hr != S_OK)goto Exit;
hr = pGraph->ConnectDirect(pFileSourceAsync_Output, pWaveParser_inputpin, NULL);//连接引脚
if (hr != S_OK)goto Exit;
hr = pWaveParser->FindPin(L"output", &pWaveParser_output);
if (hr != S_OK)goto Exit;
hr = CoCreateInstance(CLSID_DSoundRender, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (LPVOID*)&pDefaultDirectSoundDevice);//创建DefaultDirectSoundDevice过滤器
if (hr != S_OK)goto Exit;
hr = pGraph->AddFilter(pDefaultDirectSoundDevice, L"DefaultDirectSoundDevice");
if (hr != S_OK)goto Exit;
hr = pDefaultDirectSoundDevice->FindPin(L"Audio Input pin (rendered)", &pDefaultDirectSoundDevice_AudioInputpinrendered);
if (hr != S_OK)goto Exit;
hr = pGraph->ConnectDirect(pWaveParser_output, pDefaultDirectSoundDevice_AudioInputpinrendered, NULL);//连接引脚
if (hr != S_OK)goto Exit;
pControl->Run();//运行过滤器图
return;
Exit:
if (pFileSourceAsync_Output != NULL)
{
pFileSourceAsync_Output->Release(); pFileSourceAsync_Output = NULL;
}
if (pFileSourceAsync != NULL)
{
pFileSourceAsync->Release(); pFileSourceAsync = NULL;
}
if (pWaveParser_inputpin != NULL)
{
pWaveParser_inputpin->Release(); pWaveParser_inputpin = NULL;
}
if (pWaveParser_output != NULL)
{
pWaveParser_output->Release(); pWaveParser_output = NULL;
}
if (pWaveParser != NULL)
{
pWaveParser->Release(); pWaveParser = NULL;
}
if (pDefaultDirectSoundDevice_AudioInputpinrendered != NULL)
{
pDefaultDirectSoundDevice_AudioInputpinrendered->Release();
pDefaultDirectSoundDevice_AudioInputpinrendered = NULL;
}
if (pDefaultDirectSoundDevice != NULL)
{
pDefaultDirectSoundDevice->Release(); pDefaultDirectSoundDevice = NULL;
}
if (pIBasicAudio != NULL)
{
pIBasicAudio->Release(); pIBasicAudio = NULL;
}
if (pControl != NULL)
{
pControl->Release(); pControl = NULL;
}
if (pGraph != NULL)
{
pGraph->Release(); pGraph = NULL;
}
代码中每一步都添加了判断代码,和安全退出代码。实际应用程序必须这样做,因为,即使某些代码没有成功,也不至于导致程序崩溃,只是任务没有完成。但下面的示例都省略了判断代码,目的是为了代码简洁。后面的所有示例,全部在32位MFC对话框应用程序中实现。
编写DirectShow应用程序需要
1.包含dshow.h头文件,导入strmiids.lib库文件。
#include "dshow.h"
#pragma comment(lib, "strmiids")//导入库,包含类标识定义
2.启动程序时初始化COM库,在程序退出时,关闭COM库。
HRESULT hr = CoInitialize(NULL);//初始化COM库
if (hr != S_OK)
{
AfxMessageBox(_T("COM库初始化失败!"));
}
CoUninitialize();//关闭COM库
因为过滤器图管理器,过滤器都是COM对象。编者将CoInitialize放在了应用程序类的InitInstance函数中,在主对话框DoModal函数前;将CoUninitialize放在ExitInstance函数中。
编写DirectShow应用程序
编写DirectShow应用程序,就是做以下工作:
1.创建过滤器图管理器。
2.创建过滤器。
3.将过滤器添加到图中。
4.配置过滤器。一些过滤器需要配置代码。
5.连接引脚。
6.处理过滤器图事件。
7.运行和测试过滤器图。
创建过滤器图管理器
IGraphBuilder* pGraph=NULL;//声明过滤器图管理器接口指针
HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGraph));
//创建过滤器图管理器,如果成功返回S_OK
或
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder,(LPVOID*)&pGraph);
//参数1,CLSID_FilterGraph 是过滤器图管理器的类标识
//参数2,IUnknown接口指针。如果为NULL,则表示该对象不是作为聚合的一部分创建的
//参数3,CLSCTX_INPROC_SERVER 为CLSCTX枚举值,表示创建和管理此类对象的代码是一个DLL
//参数4,要获取的接口的标识
//参数5,指针变量的地址,接收获取到的接口指针
//IID_PPV_ARGS 宏,根据所使用的接口指针的类型自动提供所请求接口的IID值
过滤器图管理器为过滤器图创建了一个工作线程,工作线程并不是这些创建代码所在的线程。
创建过滤器
DirectShow Filters类别下的过滤器都可以使用CoCreateInstance函数创建。但音频压缩类别(Audio Compressors)视频压缩类别(Video Compressors)下的过滤器必须使用系统设备枚举器创建。原因是这些过滤器使用了硬件,目的是获得较高的转换速度。
使用CoCreateInstance函数创建过滤器:
IBaseFilter *pFilter = NULL;
GUID guid = { 0xd51bd5a1, 0x7548, 0x11cf, 0xa5, 0x20, 0x00, 0x80, 0xc7, 0x7e, 0xf5, 0x8a };
//要创建的过滤器的GUID
HRESULT hr = CoCreateInstance(guid, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFilter));
或
hr = CoCreateInstance(guid, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (LPVOID*)&pFilter);
//创建过滤器,要求要创建的过滤器已注册。如果创建成功,返回值为S_OK。pFilter接收过滤器的指针
//参数1,是过滤器的GUID(类标识)
//参数2,IUnknown接口指针。如果为NULL,则表示该对象不是作为聚合的一部分创建的
//参数3,CLSCTX枚举值。CLSCTX_INPROC_SERVER表示创建和管理此类对象的代码是一个DLL
//参数4,要获取的接口的标识
//参数5,指针变量的地址,接收获取到的接口指针
//IID_PPV_ARGS 宏,根据所使用的接口指针的类型自动提供所请求接口的IID值
使用系统设备枚举器创建过滤器:
IBaseFilter* pFilter=NULL;//声明过滤器接口
ICreateDevEnum* pSysDevEnum = NULL;//声明系统设备枚举器接口
HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSysDevEnum));//创建系统设备枚举器。
IEnumMoniker* pEnumCat = NULL;
hr = pSysDevEnum->CreateClassEnumerator(CLSID_AudioCompressorCategory, &pEnumCat, 0);//创建指定类别的类枚举器
IMoniker* pMoniker = NULL;
ULONG cFetched;
while (pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK)
{
IPropertyBag* pPropBag;
hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pPropBag);
if (hr == S_OK)
{
VARIANT varName;
VariantInit(&varName);
hr = pPropBag->Read(L"FriendlyName", &varName, 0);//检索过滤器的名称
if (hr == S_OK )
{
CString Friendly_Name = varName.bstrVal;
if (Friendly_Name == L"CCITT A-Law" )//如果找到指定的设备
{
hr = pMoniker->BindToObject(NULL, NULL, IID_PPV_ARGS(&pFilter));//创建过滤器的实例
VariantClear(&varName);//清除变量
pPropBag->Release();//释放IPropertyBag接口
pMoniker->Release();//释放IMoniker接口
pEnumCat->Release();//释放 IEnumMoniker接口
pSysDevEnum->Release();//释放系统设备枚举器接口
return S_OK;
}
}
VariantClear(&varName);//清除变量
pPropBag->Release();//释放IPropertyBag接口
}
pMoniker->Release();//释放IMoniker接口
}
pEnumCat->Release();//释放 IEnumMoniker接口
pSysDevEnum->Release();//释放系统设备枚举器接口
创建DMO过滤器:
#include "dmodshow.h"
#include "dmoreg.h"
#pragma comment(lib, "dmoguids.lib")
IBaseFilter* pFilter=NULL;
hr = CoCreateInstance(CLSID_DMOWrapperFilter, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, reinterpret_cast<void**>(&pFilter));
//创建DMO包装器过滤器
IDMOWrapperFilter* pDmoWrapper = NULL;
hr = pFilter->QueryInterface(IID_IDMOWrapperFilter,reinterpret_cast<void**>(&pDmoWrapper));//查询IDMOWrapperFilter接口
GUID G_DMO = { 0x2eeb4adf, 0x4578, 0x4d10, 0xbc, 0xa7, 0xbb, 0x95, 0x5f, 0x56, 0x32, 0x0a };//DMO的类标识
hr = pDmoWrapper->Init(G_DMO, DMOCATEGORY_AUDIO_DECODER);//初始化过滤器
pDmoWrapper->Release();
将过滤器添加到图中
hr = pGraph->AddFilter(pFilter, L"WMAudio DMO");//添加到过滤器图中
配置过滤器
不同的过滤器有着不同的配置代码,一些过滤器不需要配置带码。以上面的播放wav音频文件代码为例,为异步文件源过滤器指定读取文件,就是它的配置代码:
IFileSourceFilter* pIFileSourceFilter1 = NULL;
hr = pFileSourceAsync->QueryInterface(IID_PPV_ARGS(&pIFileSourceFilter1));//获取IFileSourceFilter接口
if (pIFileSourceFilter1 != NULL)
{
hr = pIFileSourceFilter1->Load(L"D:\\音频视频\\音频\\不同编码方式的WAV\\PCM.wav", NULL);//加载媒体文件
pIFileSourceFilter1->Release(); pIFileSourceFilter1 = NULL;
}
连接引脚
要连接引脚,首先需获取引脚。如果知道引脚的标识,可以使用IBaseFilter接口的FindPin方法获取引脚:
IPin* pOutPin = NULL;
hr = pFilter1->FindPin(L"Output", &pOutPin);//如果成功,返回S_OK。pFilter1的类型为IBaseFilter*
//参数1,引脚的标识。大部分过滤器其引脚标识就是引脚名称
//参数2,接收引脚IPin接口指针的指针变量
如果不知道过滤器引脚标识,可以枚举过滤器所有引脚,获取其标识:
IEnumPins* pEnum = NULL;
hr = pFilter1->EnumPins(&pEnum);//获取IEnumPins接口指针
IPin* pPin = NULL;
while (pEnum->Next(1, &pPin, 0) == S_OK)
{
LPWSTR lp;
pPin->QueryId(&lp);//如果成功,lp包含引脚标识
CoTaskMemFree(lp);//必须使用CoTaskMemFree释放它
pPin->Release();//在下一次循环前释放该引脚
}
pEnum->Release();//释放枚举引脚接口
连接引脚:
hr = pGraph->Connect(pOutPin, pInPin);//pGraph为过滤器图管理器指针,类型为IGraphBuilder*
//参数1,输出引脚的指针。类型为IPin*
//参数2,输入引脚的指针。类型为IPin*
如果引脚连接成功,IGraphBuilder的Connect方法返回S_OK。此方法可能拉入中间过滤器。如果直接连接两个引脚,可以使用下面方法:
AM_MEDIA_TYPE mt;
mt.majortype = MEDIATYPE_Video;
mt.subtype = MEDIASUBTYPE_RGB24;
hr = pGraph->ConnectDirect(pOutPin, pInPin, &mt);//可以提供媒体类型的所有信息;也可以只指定部分信息
ConnectDirect是IFilterGraph方法,直接连接两个引脚。参数3为媒体类型结构AM_MEDIA_TYPE的指针,指定连接使用的媒体类型。可以指定结构的全部参数,也可以只指定部分参数,也可为NULL。为NULL时,由两个引脚协商连接使用的媒体类型。
少部分过滤器须指定媒体类型的全部参数,这时可以枚举输出引脚的媒体类型,当获取到希望的类型时,使用该媒体类型连接引脚:
IEnumMediaTypes* pEnum = NULL;
hr = pOutPin->EnumMediaTypes(&pEnum);
AM_MEDIA_TYPE* pmt = NULL;
while (hr = pEnum->Next(1, &pmt, NULL), hr == S_OK)
{
if (pmt->majortype == MEDIATYPE_Audio && pmt->subtype == MEDIASUBTYPE_PCM)break;
_DeleteMediaType(pmt);//必须删除获取到的媒体类型结构,包括它的格式块
}
pEnum->Release();
hr= pGraph->ConnectDirect(pOutPin, pInPin,pmt);
//下面是_DeleteMediaType和_FreeMediaType函数定义
void _FreeMediaType(AM_MEDIA_TYPE& mt)//释放媒体类型的格式块。
{
if (mt.cbFormat != 0)
{
CoTaskMemFree((PVOID)mt.pbFormat);
mt.cbFormat = 0;
mt.pbFormat = NULL;
}
if (mt.pUnk != NULL)
{
mt.pUnk->Release();
mt.pUnk = NULL;
}
}
void _DeleteMediaType(AM_MEDIA_TYPE* pmt)//删除在堆上分配的媒体类型结构。
{
if (pmt != NULL)
{
_FreeMediaType(*pmt);
CoTaskMemFree(pmt);
}
}
连接是否成功主要在于,要连接的输入引脚和输出引脚允许类型中,是否具有相同的媒体类型;或指定的媒体类型是否为两引脚共同允许的类型。如果是,大部分情况下连接可以成功。但极少情况下,即便有共同的媒体类型,仍无法连接。
如果连接成功,大部分情况下可以正常运行。但存在连接即便成功仍无法运行的情况,这种情况比较少见,但是存在。原因是两个过滤器功能不匹配。
处理过滤器图事件
要处理过滤器图事件,首先要获取图事件。
获取过滤器图事件:
1.获取媒体事件接口;获取媒体事件扩展接口。
IMediaEvent* pEvent = NULL;
IMediaEventEx* pEventEx = NULL;
hr = pGraph->QueryInterface(IID_PPV_ARGS(&pEvent));//获取媒体事件接口
hr = pGraph->QueryInterface(IID_PPV_ARGS(&pEventEx));//获取媒体事件扩展接口
2.定义一个私人消息(WM_GRAPHNOTIFY),如果过滤器图有事件通知,将WM_GRAPHNOTIFY消息发送到指定窗口。
#define WM_GRAPHNOTIFY WM_APP + 1 //应用程序可以将WM_APP到0xBFFF范围内的消息编号用作私有消息
HWND g_hwnd=GetSafeHwnd();//接收WM_GRAPHNOTIFY消息的窗口的句柄
hr=pEventEx->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);//指定接收WM_GRAPHNOTIFY消息的窗口
3.重写接收窗口(这里是对话框)的WindowProc函数,在函数中处理过滤器图事件。
处理过滤器图事件:
最常见的要处理的事件之一是EC_COMPLETE(流渲染完成),即使全部流渲染已完成,过滤器图状态仍为运行状态,这时我们需要停止过滤器图。
LRESULT CMFCApplication1Dlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
long evCode; LONG_PTR param1, param2;
switch(message)
{
case WM_GRAPHNOTIFY:
pEvent->GetEvent(&evCode, ¶m1, ¶m2, 0);
switch(evCode)
{
case EC_COMPLETE://播放完成
pControl->Stop();//停止过滤器图
break;
case EC_ACTIVATE://播放窗口已激活
break;
case EC_ERRORABORT://发生错误,操作被中止
break;
}
pEvent->FreeEventParams(evCode, param1, param2);//释放为事件参数分配的资源
break;
}
return CDialogEx::WindowProc(message, wParam, lParam);
}