DirectShow 媒体文件回放总结
MU小组文档
作者:Inkick
本文99%内容原创,转载请注明出处
DirectShow中媒体文件回放的过程也就是一个为媒体文件选择相应所需的Filter、构建Filter Graph、并对Filter Graph的状态进行维持、控制的过程。这里所说的媒体文件,不仅仅是指音频、视频文件,同时也包括bmp、jpeg、gif等图形图像格式以及midi等数字化音乐序列。
因此,使用DirectShow进行媒体文件的回放需要经过以下的步骤:
Filter Graph为Filter提供了一个容器,一个构建完整的Filter Graph也就是一个完整的Filter连路,这个连路对于程序是透明的,可控制的。而对于每一个媒体文件来说,Filter Graph与媒体文件存在着对应的关系。也就是说,一个Filter Graph只能实现一个(种)文件的回放。
在DirectShow中,Filter Graph是由接口对象IGraphBuilder实现的,我们可以调用Win32 API函数CoCreateInstance()建立一个实体。Filter Graph实体建立之后并不具有任何的Filter,因此不具有任何实际用途。因此我们需要连接需要的Filter来完成FilterGraph的构建。
智能连接这个术语覆盖了一系列Filter Graph Manager用于构建所有或部份filter graph的算法。任何时候,当Filter Graph Manager需要添加filter来完成graph时,它大致做以下几件事情:
如果有一个filter存在于graph中,而且这个filter有至少一个没有连接的input pin,Filter Graph Manager试着去试用这个filter。
否则,Filter Graph Manager在已注册的filter中寻找连接时可以接受合适的媒体类型的filter。每一个filter都注册有一个Merit值,这个值用以标记哪个filter最容易被Filter Graph Manager选中来完成graph。Filter Graph Manager按Merit值的顺序来选择filter,Merit值越大,被选中的机会越大。对于每种流类型(如音频、视频、MIDI),默认的renderer具有一个很高的Merit值,解码器同样是,专用filter具有低Merit值。
如果Filter Graph Manager选择的filter不合适,它会返回来尝试另外的filter组合。
我们有三种构建graph的途径:
1.filter graph manager构建整个graph
2.filter graph manager构建部分graph
3.应用程序构建整个graph
IGraphBuilder提供了多种智能完成FilterGraph构建的方法。最简单的是使用接口方法IGraphBuilder::RenderFile。
HRESULT RenderFile(LPCWSTR lpwstrFile, LPCWSTR lpwstrPlayList);
第一个参数为文件的路径(祥见后文),第二个参数保留,必须为空。
这个方法需要一个表示媒体文件路径或者URL的Unicode字符串参数。而我们通过界面获得的文件路径的字符串往往是ANSI字符串。我们可以使用下面方法进行转换:
包含头文件:
#include
#include
这两个头文件包含了ANSI字符串与Unicode字符串相互转化的函数与宏
使用宏:USES_CONVERSION;
定义一个WCHAR的数组:WCHAR FileName[MAX_PATH];
而MAX_PATH是在windef.h中定义的:#define MAX_PATH 260
这与windows路径最大字符为260个相符。这个数组保存转化后的Unicode形式表示的路径。然后可以使用下面的函数进行转换:(假定以ANSI形式给出的字符串为szFile)
wcsncpy (FileName, T2W(szFile), NUMELMS(wFile)-1);
FileName[MAX_PATH-1] = 0;
wcsncpy的原型:
wchar_t *wcsncpy( wchar_t *strDest, const wchar_t *strSource, size_t count );
这个函数的作用类似于strcpy,是实现字符串之间的复制。只不过,这是一个用在Unicode上的版本。
第一个参数指定了字符串转化后的存放地址,也就是我们要得到的Unicode字符串,第二个参数指定了要转化的字符串的来源地址,也就是我们要转化的ANSI字符串。在第二个参数使用了宏T2W(szFile),这个宏可以把一个ANSI字符串转为一个WCHAR类型的字符串。第三个参数为转化字符串中字符的数量。
现在问题出来了,这个函数的第二个参数需要宽字符串的地址,如果我们有这样的一个地址,我们还转换什么?因此这个的关键在于T2W上面。让我们来看一下T2W的定义。这个定义在头文件ATLCONV.H里面(只保留我们比较感兴趣的部分)。
#ifdef _UNICODE
inline LPWSTR T2W(LPTSTR lp) { return lp; }
inline LPTSTR W2T(LPWSTR lp) { return lp; }
#else
#define T2W A2W
#endif
我们可以看到,如果定义了_UNICODE,则T2W直接返回将要转化的字符串,这是因为在Unicode环境下,ANSI字符拥有和Unicode字符同样的宽度。也就是说,ANSI是Unicode的一个子集。但是在非Unicode环境下,就将T2W替换成A2W,这样我们返回来看A2W的定义(这个定义在同样的头文件中):
#define A2W(lpa) (/
((LPCSTR)lpa == NULL) ? NULL : (/
_convert = (lstrlenA(lpa)+1),/
ATLA2WHELPER((LPWSTR) alloca(_convert*2), lpa, _convert)))
宏定义比较晦涩,我们转换成比较好理解的函数形式:
LPCSTR A2W(LPTSTR lpa)
{
if(lpa == NULL)
{
return NULL;
}
_convert = (lstrlen(A)(lpa)+1);
AtlA2WHelper ((LPWSTR)alloca(_covert*2),lpa,_convert);
return lpa;
}
这个函数的结构比较清晰,首先判断是不是空字符串,如果是空字符串就返回空,因为ANSI和Unicode意义上的空字符串都是NULL,如果不为空,则开始转换。重点是函数 ATLA2WWHELPER(),这个函数的实现部分在ATLCONV.CPP中:
LPWSTR WINAPI AtlA2WHelper(LPWSTR lpw, LPCSTR lpa, int nChars)
{
_ASSERTE(lpa != NULL);
_ASSERTE(lpw != NULL);
lpw[0] = '/0';
MultiByteToWideChar(CP_ACP, 0, lpa, -1, lpw, nChars);
return lpw;
}
这个函数除了做一些必要的安全性判定以及前序准备以外,核心工作是调用了一个函数MultiByteToWideChar,因此我们还要继续深入。这次我们发现我们找不到源代码了,但是在MSDN中我们可以得到明确的提示:
int MultiByteToWideChar(
UINT CodePage, // code page
DWORD dwFlags, // character-type options
LPCSTR lpMultiByteStr, // address of string to map
int cchMultiByte, // number of bytes in string
LPWSTR lpWideCharStr, // address of wide-character buffer
int cchWideChar // size of buffer
);
这个API函数接受六个参数(Win32 API的风格——参数超多),第一个参数指定了CodePage(解释见附录5.1),在这里我们可以指定CP_ACP,来表示我们选择ANSI Code Page代表我们要转换的源字符串编码形式为ANSI,第二个参数是一组位标志,决定了如何处理原字符串中的控制字符或者无效字符。一般指定MB_PRECOMPOSED,第三个参数是源字符串的地址(或者指针)。第四个字参数是要转化的字符串里面包含了多少个字符,第五个参数指定了转换后的字符串的存放地址,第六个参数指定了转换后字符串的Buffer的大小,也就是转换后占用内存空间的多少。
现在我们逐层返回,(如果你已经忘记了我们的初衷是什么建议你听一下F.I.R的歌),根据最核心的函数MultiByteToWideChar的分析,我们可以得出AtlA2Whelper几个参数的含义:
AtlA2WHelper( LPWSTR lpw, //转化的目标字符串的地址
LPCSTR lpa, //转化的源字符串的地址
int nChars //源字符串中包含的字符数
)
因此在判断lpw以及lpa不为空之后便直接调用:
lpw[0] = '/0';
MultiByteToWideChar(CP_ACP, 0, lpa, -1, lpw, nChars);
我们已经返回到了那个宏,还是让我们来看我们改写的那个函数吧:
_convert = (lstrlen(A)(lpa)+1);
AtlA2WHelper ((LPWSTR)alloca(_covert*2),lpa,_convert);
return lpa;
lstrlen函数得到参数字符串的长度(对于ANSI字符串来说是字节数,对于Unicode字符串来说是字符数),后面的(A)表明参数字符串为ANSI字符串,经过这个参数的调用,_convert的值变成了要转换的字符串长度加一。为什么要加一呢?因为字符串的结尾要补上一个’/0’,我们总要为这个’/0’预留空间。
下一步就是要申请空间来保存转化完成的字符串了,使用(LPWSTR)alloca(_covert*2),分配字符串数的两倍空间(因为Unicode字符占用的空间是ANSI的两倍),然后将地址转为LPWSTR,当作参数传递给AtlA2Whelper,开始转换,任务完成!
可是,真的完成了吗?那个该死的_convert是哪里来的?哈哈,还记得我们一开始说的吗?回忆一下,在使用T2W之前要做的工作是什么?使用宏USES_CONVERSION;!为什么呢?我们继续看这个宏的定义:
#ifndef _DEBUG
#else
#define USES_CONVERSION int _convert = 0
#endif
看到了吗?这个宏其实就是在定义这个_convert变量。因此,当你在不同的作用域中使用T2W宏的时候,一定要在使用前写上这个东东。
闭上眼睛,整理一下思路,所有技术文档上面几行说清楚的RenderFile,被我洋洋洒洒写了将近三页。中间涉及的相关内容完全可以写成一份技术白皮书。如果这两页能够说清楚什么的话,我还是很有成就感的。如果对Unicode以及ANSI感兴趣,请自行查阅相关的资料,我发誓这辈子都不想碰到类似的问题了。
实例代码:见附录5.2,RenderFile
回到我们亲爱的DirectShow(来个拥抱吧!)。
这个接口方法从一个输出Pin开始,自动构建剩余的FilterGraph。这个方法自动添加必要的Filter,直到添加到一个Render Filter为止。
HRESULT Render( IPin *ppinOut);
使用这个接口方法首先要找到一个未连接的输出Pin,所谓的Pin是指Filter上的数据流入或者流出的端口,一个Filter上至少有一个Pin,根据Pin数据流的方向可以把Pin分为输入Pin和输出Pin两种。我们查找Pin一般遵循下面的步骤:
1, 枚举目标Filter上面所有的Pin。
IPin * pPin
IenumPins *pEnum = NULL;
pFilter->EnumPins(&pEnum);
while(pEnum->Next(1,&pPin,NULL)==S_OK)
{
//TODO: your process here
}
第一行代码首先声明了一个枚举器指针,然后第二行调用Filter上的接口方法生成一个实例,接着是用一个While循环开始枚举,每循环一次,pPin指针指向下一个Pin,然后针对每一个Pin,进行以下处理:
2, 判断每一个Pin的连接状态和连接方向。
判断Pin的连接方向:
PIN_DIRECTION PinDir;
pPin->QueryDirection(&PinDir);
//TODO : test the direction here
第一行代码定义了一个类型为PIN_DIRECTION的变量PinDir,然后在第二行调用接口方法pPin->QueryDirection(&PinDir);调用后,PinDir的值就成为了pPin上的方向。然后根据实际需要进行判断处理。
判断Pin的连接状态:
IPin *pTemp;
If(SUCCEEDED(pPin->ConnectedTo(&pTemp)))
{
//it is not the just pin we want,because it is has been connected
}
else
{
//this pin is we want
}
判断Pin的连接状态我们使用了接口方法pPin->ConnectedTo(&pTemp),这个接口方法的真正用途是返回和这个Pin连接的另一个Pin的指针。如果没有其他的Pin和这个Pin连接,就返回E_FAIL,因此我们可以使用SUCCEEDED宏来捕获接口方法的返回值,以判断Pin的连接状态。
在获得某个Filter上没有连接而且方向符合数据流向的Pin之后,我们就可以调用这个Render接口方法完成这个Filter Graph的构架。
一般来说,这个接口方法最好配合IGraphBuilder::AddSourceFilter使用,也就是首先加入一个源Filter,然后从这个源Filter的输出Pin开始,完成FilterGraph的构架。
AddSourceFilter方法添加一个能render特定文件的source filter。首先,它依据协议名、文件扩展名、或文件头在已注册的filter中寻找匹配的那个。如果此方法定位到了一个合适的source filter,它便立刻创建一个这个filter的实例,并将其添加到graph中,然后调用filter的IFileSourceFilter::Load方法。
实例代码:见附录5.4
与Render File以及Render等处于Filter Graph的全局地位的完成方式相比,IgraphBuilder也提供了多种面向局部,具体而微的接口方法。这些接口方法大多是针对两个Filter或者两个Pin之间的连接。ConnectDirect方法就是其中之一。这个方法尝试将两个Filter直接相连,如果不成功,直接返回。不作其他处理。这个接口方法我们一般用作两个Pin之间的协商检查,而不直接用于两个Filter的连接。原因就在于这个函数成功率太低,如果用于两个Filter的连接需要反复尝试不同的Filter,将严重影响效率。当然,如果程序可以明确两个Filter可以连接,使用这个接口方法可以使用一种简洁的方法完成连接。
HRESULT ConnectDirect( IPin *ppinOut, IPin *ppinIn, const AM_MEDIA_TYPE *pmt);
前两个参数是要连接的两个Filter的输入输出Pin,最后一个是媒体类型的结构,可以为空。关于媒体结构,请参考附录5.2。
这个接口方法是ConnectDirect接口方法的延伸和扩展。该接口方法同样是连接两个Filter,但是与ConnectDirect方法不同的是,这个接口方法首先尝试ConnectDirect方法连接两个Filter,如果不能直接连接(这种情况多数由于媒体类型不匹配),则尝试使用中间Filter连接。因此,这个接口方法的执行成功率较高。
这个接口方法将指定的Filter加入到Filter Graph中,但这个新加入的Filter并不会和Filter Graph中的其他Filter形成连接。与此类似,将Filter Graph中的某个Filter与其他Filter连接的断开,并不会使之脱离Filter Graph,要是Filter脱离Filter Graph,必须要在断开连接之后,调用RemoveFilter接口方法。
手工构建Filter Graph的基础在于对于媒体文件所需要的Filter有着相当深入清晰地了解的基础上。否则,手工构架的FilterGraph将存在可靠性、健壮性等问题。
手动构架Filter Graph主要用到上面介绍的几个接口方法:AddFilter、Connect、以及(很少情况)ConnectDirect。这几个接口方法在上文没有详细说明的原因也是因为如此,放在这里做一个系统的交代。
主要思路如下:
n 根据文件类型或者设定的Filter Graph选择Filter
选择Filter主要通过枚举系统存在、支持的Filter,并根据预先的设定或者在用户参与的条件下使用用户所选择的Filter
n 将选中的Filter加入到Filter Graph中
这一步主要使用上面的几个接口方法实现,具体参看附带的实例代码。
n 如果连接过程中出现问题,尝试加入中间Filter,或者替换Filter。
n 完成连接。
我们不推荐整个Filter Graph均由手工构建,一种既能充分利用DirectShow智能化又能体现Filter Graph的针对性的方法是,使用AddSourceFilter加入源Filter,然后开始手工连接(当然,这种方法不适用需要使用非标准非默认文件源Filter的Filter Graph)。
实例代码(暂时空缺):
对Filter Graph的控制包括对Filter Graph的状态的控制,对媒体文件视频、音频的控制等。下面分三部分介绍:
在介绍对Filter Graph状态的控制之前,首先要说明的是Filter Graph的状态。Filter Graph在任何时候只有三种状态:运行、暂停、停止。这三种状态的标明了媒体文件回放时的状态。
当刚刚为一个媒体文件构建一个Filter Graph时,这个Filter Graph处于停止状态,随着文件的播放,Filter Graph也转入运行状态(事实上是Filter Graph转入运行状态导致文件开始播放)。当Filter Graph进入暂停状态文件的播放也随之暂停。当文件播放完毕时,Filter Graph重新回到停止状态。(这段描述只是为了表明文件播放的状态和Filter Graph的状态的对应关系,不能正确地反映两者相互作用相互影响的顺序和关系)
因此,我们对Filter Graph状态的控制也就集中在三种状态之间的切换。
对于Filter Graph状态的控制我们需要使用另外一个接口:IMediaControl,这个接口的实现在IFilterGraph上,但是应用程序使用时,要在IGraphBuilder 上申请这个接口。
pGraph->QureyInterface(QueryInterface(IID_IMediaControl, (void **)&pControl);
获得这个接口后,我们就可以使用上面的接口方法(SDK文档上面这个接口有九个接口方法,但是我们只能用前五个,后四个是为VB实现的,VB真好!):
IMediaControl::Run();
IMediaControl::Pause();
IMediaControl::Stop();
IMediaControl:: GetState( LONG msTimeout, OAFilterState *pfs);
IMediaControl:: StopWhenReady();
这个五个接口方法中,前三个都是自解释的,而且没有任何参数,返回值是标准的HRESULT,这种函数的调用让人觉得呼吸顺畅,神清气爽。可是我们不是在森林里做有氧运动,事实上,通宵写这个文档已经让我喘不过气来了。
让我们集中精力:
第四个接口方法看起来不是那么痛苦,返回Filter Graph的状态。需要解释的是参数。第一个参数指定了一个时间段,DirectShow里面Filter Graph状态的转化并不一定是同步的,因此当你调用这个接口方法的时候,Filter Graph可能正在向一种新的状态转化,因此这个接口方法将等待至Filter Graph稳定到某一个状态或者等待的时候超过了第一个参数指定的那个时间段。不过这种不稳定的状态实在太少了(我们是在使用DirectShow而不是在研究高能物理电子轨道的跃迁),因此这个参数放心的写上0,微软设置这个参数的目的——我猜——是为了提高程序的可靠性,即便是这样微软还是有那么多的Bug,谁知道呢?第二个参数是一个指向OAFilterState的指针,这个参数的值在方法成功后成为Filter Graph的状态。
第五个接口方法就相当让人迷茫了,虽然它的命名很好J 。这个接口方法的暂停Filter Graph,等待数据处理完毕后停止Filter Graph。这个接口方法主要用途并不是在于对Filter Graph状态的控制,而是在于当Filter Graph停止时提供了一种有效的搜寻方法。具体的原理我们与IMediaSeeking接口一起介绍。
完了吗?真的完了吗?没错,但这只是一部分,只是应用程序对Filter Graph进行控制的一部分,如果Filter Graph需要与应用程序进行交互呢?
FilterGraph与应用程序之间事件的交互主要通过另外的一个接口IMediaEventEx来实现。(Filter Graph上有三个接口与事件处理有关,IMediaEventSink,用于Filter 与FilterGraph之间的事件交互,IMediaEvent与IMediaEventEx是应用程序与Filter Graph进行事件交互的接口,因为后者继承自前者,是前者的扩展,因此我们大多选用后者。)
IMediaEventEx接口与IMediaControl接口类似,实现在IFilterGraph上,但应用程序应该在IGrahpBuilder上申请。
IMediaEventEx *pEvent;
pGraph->QureyInterface(IID_IMediaEventEx,(void**)&pEvent);
Filter Graph与应用程序的事件交互有两种方式,这两种方式的区别在于事件交互的主动者不同。一种是Filter Graph主动将事件的发生通知给应用程序(窗体),一种是Filter Graph维持一个消息泵,应用程序不断的从中取出消息。在实际开发中,我们一般选中第一种,第二种方法一般用于不适合消息传递和没有窗体的场合。
首先我们自定义一个消息:
#define WM_DS_EVENT 19850415
后面的那个整数是我随便加的(其实也不是随便啦,你送我礼物我就告诉你),只要保证不与Windows内部消息冲突即可。
然后设置Graph Filter的事件通知码,设置之后,当FilterGraph中某个事件发生后Filter Graph就会将事件通知码以Windows消息的方式发给程序窗体,窗体捕捉到这个消息后,就可以调用相应的接口方法,来获知具体的事件。
pEvent->SetNotifyWindow((OAHWND)hwnd,WM_DS_EVENT,0);
这个接口方法注册一个窗口,这个窗口在Filter Graph有事件发生时成为消息的接收者,第二个参数为一个long类型,即事件发生的的消息。最后一个参数是作为lParam而接受的,这里直接给了0。因为我们只需要知道Filter Graph有事件发生了,而不需要知道更多的信息。
现在开始处理,在WinProc函数(我喜欢Win32的重要理由就是我不用写消息映射宏,我是一个懒人啊!)捕获WM_DS_EVENT消息:
case WM_DS_EVEVT:
{
if(pEvent)
{
LONG eventCode = 0, wParam = 0, lParam = 0;
while(SUCCEEDED(pEvent->GetEvent(&eventCode,&wParam.&lParam,0)))
{
pEvent->FreeEventParams(eventCode,wParam.lParam);
swtich(eventCode)
{
//process events here
}
}
}
}
这段代码调用了IMediaEventEx接口方法:GetEvent和FreeEventParams,关于这两个接口方法的说明如下:
HRESULT GetEvent( long *lEventCode, long *lParam1, long *lParam2, long msTimeout);
GetEvent是从DirectShow Filter Graph的消息泵中取出一条消息,这个接口方法成功后,第一个参数表明了事件消息,也就是发生了什么事件。第二个第三个参数表明了一些辅助信息,根据事件的不同含义也有所不同。最后一个参数的含义与IMediaControl::GetStates接口方法的事件参数类似,给0表示方法立即返回。
HRESULT FreeEventParams( long lEventCode, long lParam1, long lParam2);
这个接口方法释放GetEvent调用时为参数分配的空间,注意,只是回收空间,参数的值没有变化,因此我们建议在调用GetEvent之后应该立即调用这个接口方法,以免造成资源的浪费,甚至内存泄漏。
经过这些之后,eventCode就代表了事件的类型。然后随便怎么处理吧。在DirectShow中常见的事件有:
n EC_COMPLETE表示媒体文件已经回放完毕。但此时Filter Graph并不直接转入停止状态,需要我们调用IMediaControl::Stop()方法。而且Filter Graph没有提供给我们循环播放一个文件的方法,因此我们需要对这个事件进行处理。参加附录5.3
n EC_ERRORABOR表示Filter Graph运行出错
n EC_USERABORT表示用户中止了文件的回放
n EC_REPAINT表示视频窗口IVideoWindows需要重画当前帧。
除此以外,IMeidaEventEx接口上还有其他接口方法。
IMediaEventEx上的接口方法:
n HRESULT SetNotifyFlags(long lNoNotifyFlags);设置事件通知码。
n HRESULT GetNotifyFlags(long *lplNoNotifyFlags);得到事件通知码。
IMediaEvent上的接口方法:
n HRESULT CancelDefaultHandling( long lEvCode);为指定的消息取消默认的消息处理。在取消后,这条消息将传递给应用程序,必须由应用程序手动处理。
n RestoreDefaultHandling( long lEvCode)为指定的消息恢复默认的消息处理
n WaitForCompletion( long msTimeout, long *pEvCode);Filter Graph挂起,直到文件回放完毕。
到目前为止,对于文件回放的Filter Graph控制与交互的接口涉及到IMediaControl和IMediaEventEx,这两个接口相互配合,完成了应用程序与Filter Graph的协调合作。
现在的问题放在媒体文件的搜寻上,我谨慎的认为这一部分应该属于对Filter Graph的状态的控制,而并非对媒体文件视频或者音频的控制。让我们使用程序员的语言吧,这总是让人觉得很美妙。
对于媒体文件的搜寻,DirectShow提供了另外的一个接口IMediaSeeking,(数一下,我们已经遇到了多少个接口?)这个接口的方法众多,不过对于文件回放来说,我们似乎只需要用到不多的几个,但谁能保证其余的就不会用到呢?
与时间相关的接口方法:
n HRESULT GetCapabilities(DWORD *pCapabilities);这个接口方法返回当前媒体文件类型是否支持搜寻。参数在接口方法调用后成为当前Filter Graph的搜寻能力的标示。这个指针指向一个AM_SEEKING_SEEKING_CAPABILITIES类型的标志。这个标志为一组枚举值,含义自解释。
n HRESULT CheckCapabilities(DWORD *pCapabilities);这个接口方法将检查参数所代表的搜寻是否被支持。接口方法返回时,参数指针内容中将是支持的搜寻能力。
n HRESULT IsFormatSupported( const GUID *pFormat);检查参数所代表的时间格式是否支持,如果支持返回S_O。时间格式参见附录5.6
n HRESULT QueryPreferredFormat( GUID *pFormat);返回首选的时间格式。
n HRESULT GetTimeFormat(GUID *pFormat);返回当前的时间格式
n HRESULT IsUsingTimeFormat( const GUID *pFormat);返回是否使用参数表示的时间格式。如果是返回S_OK。
n HRESULT SetTimeFormat(const GUID *pFormat);设置当前使用的时间格式。
n HRESULT ConvertTimeFormat(LONGLONG *pTarget, const GUID *pTargetFormat, LONGLONG Source, const GUID *pSourceFormat);将一种时间格式下的时间值转为另一种时间格式下的时间值。第一个参数保存转换后时间值的存放的地址。第二个参数以及第四个参数分别指定了目标格式和源格式。第三个参数指定了源时间值。
关于搜寻范围的接口方法:
n HRESULT GetDuration(LONGLONG *pDuration);返回文件的长度,以当前时间格式为单位
n HRESULT GetStopPosition(LONGLONG *pStop);返回停止的位置。以当前时间格式为单位。
n HRESULT GetCurrentPosition(LONGLONG *pCurrent)返回当前位置,当前时间格式为单位。
n HRESULT GetPositions(LONGLONG *pCurrent,LONGLONG *pStop);返回当前位置和停止位置,以当前时间为单位。
n HRESULT SetPositions(LONGLONG *pCurrent,DWORD dwCurrentFlags, LONGLONG *pStop, DWORD dwStopFlags);设置停止位置,当前位置。以当前时间格式为单位。
n HRESULT GetAvailable(LONGLONG *pEarliest, LONGLONG *pLatest);设置有效的搜寻范围,第一个参数为范围的开始,第二个参数为范围的结束。以当前的时间格式为单位。
关于播放速率的接口方法:
n HRESULT SetRate(double dRate);取得播放速率,正常速率为1
n HRESULT GetRate(double *dRate);设置播放速率
这些接口配合使用基本上实现了对媒体文件的搜寻,在开发中应该根据实际情况灵活运用,不要绝对的使用某一种时间格式或者接口方法。
现在返回来让我们看一个历史遗留问题,我都差点忘了。如果你也忘了,跳过这里。嘿嘿。
IMediaControl:: StopWhenReady();这个接口方法主要用于当Filter Graph停止时对媒体文件的搜寻。Filter Graph一旦停止,对于当前位置的改变VideoWindow不会自动重绘,因此,对IMediaSeeking::SetPositions接口方法的调用不会更新Video Window。因此如果要在完成对媒体文件的搜寻之后显示一帧新帧,需要调用这个接口方法。这个方法首先将Filter Graph转入暂停状态,然后等待操作完成后重新将Filter Graph转入停止状态。在暂停状态的时候数据在仍然在graph中传递,因此Video Render将不断地播放新帧。
终于开始了一个新的话题,而且这个话题比起上一个涉及了3个接口的话题要轻松许多,虽然也涉及了两个接口和很多接口方法,但是要具体很多。让我用台灯烤一下手,然后开始。
视频控制包含两个内容:一个是对于视频流的控制,一个是对于视频窗口的控制。对于视频流的控制我们更关心数据的格式、走向、有效等信息,而对于视频窗口而言,我们则更关注与视频文件播放的尺寸、位置等外观因素。
对于视频流的控制我们使用IBasicVideo接口,IBasicVideo 接口继承自IDispatch,这个接口的实现在Video Render上,但是应用程序要在Filter Graph上申请这个接口。和以前一样,这样申请一个接口:
pGraph->QueryInterface(IID_IBasicAudio, (void **)&pBasicAudio);
在说明这个接口上的接口方法之前,我们首先明确几个必要的概念:
源矩形:源矩形是指要被显示的原始图像的一部分
目标矩形:目标矩形是指Video Windows接受源矩形的一部分
视频矩形:视频矩形是指原始的视频图像。
也就是说,Video Render捕获原始图像形成源矩形,然后将源矩形拉伸或者压缩使之符合目标矩形的大小。所有矩形的单位都是像素。
这三个矩形中,视频矩形由媒体文件决定,而其他的两个则可以由我们根据实际需要程序设置,IBasicVideo提供了如下方法对源矩形和目标矩形进行控制,这些方法都是自解释的。
n get_/put_DestinationHeight
n get_/put_DestinationLeft
n get_/put_DestinationTop
n get_/put_DestinationWidth
n get_/put_SourceHeight
n get_/put_SourceLeft
n get_/put_SourceTop
n get_/put_SourceWidth
n get_VideoHeight
n get_VideoWidth
n SetSourcePosition
n SetDestinationPosition
除此以外,IBasicVideo接口提供了与媒体文件视频属性密切相关的接口方法:
n get_AvgTimePerFrame 得到每一帧的平均时间
n get_BitErrorRate 得到位出错率。这里的出错是指进行编码校验出错。
n get_BitRate 得到位速率,单位bps
另外一个比较重要的方法是得到当前帧:
GetCurrentImage ( long *pBufferSize,long *pDIBImage);第一个参数为图片的大小,可以根据视频矩形的大小、位深加上BMP文件头信息的大小计算出来,第二个参数在方法返回时指向一个位图的信息。
IBasicVideo给我们提供了较为底层和深入的控制视频流的方法,尽管如此,我们实际开发中使用IBasicVideo的机会并不多。而对于与播放显示视频流的窗口息息相关的IVideoWindow才是我们关注的重点。
IVideoWindow在Graph以及Video Render上都有实现,但应用程序应该在Filter Graph上申请这个接口。
pGraph->QueryInterface(IID_IVideoWindow, (void **)&pVideoWindow);
具体部分参看SDK文档。
这是我们最后一个话题了,对于声音的控制。你无法让你的同伴闭上嘴,但是你可以随意设置你的媒体文件音量的大小、声道的平衡。一个优秀的程序员在程序中可以获得上帝一样的权利。
相对视频流的控制,音频流的控制则要简单明确的多。首先是我们使用的接口:IBasicAudio。这个接口和往常一样在Audio Render上实现,但是应用程序需要在Filter Graph上申请这个接口。
接口方法以及功能说明如下:
n HRESULT get_Balance(long* plBalance);得到当前音频流声道的平衡度。接口方法调用成功后,参数指针的引用的值为一个范围从-10000到+10000的长整数。这个值的含义是:-10000表示右声道无声,+10000表示左声道无声,0表示声道平衡。
n HRESULT put_Balance(long lBalance);设置当前音频流的声道平衡度。参数的含义以及取值参考上一个接口方法的说明。
n HRESULT get_Volume(long* plVolume)得到音频流的音量,接口方法调用成功后,参数指针的引用的值除以100得到声音音强值,单位dB。
n HRESULT put_Volume(long lVolume);设置音频流的音量。参数的含义和取值参考上一个接口方法的说明。
好了,随着声音动听而流畅的播放,我们的回放也就完成了,就是这么简单吗?但愿吧。
对于 Filter Graph的释放来说,其实并不应该列为一个主题。但是考虑到这是一个程序的收尾工作而且对于一个安全可靠的程序是一个至关重要的保证,同时为了保证这个文件的结构符合DiretShow 媒体文件回放的步骤,才单独将其列为一个主题。
释放Filter Graph要按照以下步骤:
清空Filter Graph(不必须,建议)
释放由Filter Graph上申请的接口,将接口指针置为空
释放Filter Graph,同样将接口指针置为空。
调用CoUninitialize();
根据上述分析,我们可以得到用于媒体文件回放的DirectShow Filter Graph的接口关系图:
说明:
Audio Render上实现了IBasciAudio接口,Video Render上实现了IBasicVideo以及IVideoWindow,但是这些接口都应该从Filter Graph上申请。图中的标示方法只是为了作用关系,不代表实际的接口情况。
本附录中收集了一些相关的资料,有些来源于网上,有些是我自己的代码,有些是SDK文档的翻译整理。
所谓CodePage,就是各个可用于处理的字符集。Microsoft公司在开发MS-DOS和Windows 3.1的各文种产品时,进一步将依赖于各具体平台(IBM-Host, Macintash, MS-DOS, Windows)的各文种的字符集加以整理,并对每个具体的代码页都赋以一个代号,称作“代码页ID”。比如:
中文GB内码的代码页ID=936
GBK也是一种的代码页,其ID也是936
Bi5的代码页ID=950
Shift-JIS的代码页ID=932
而同样是日文,在Macintash 上的代码页ID=10001
广义地说,UCS/Unicode也是一种Code Page,其代码页ID=1200。但在严格意义上,UCS/Unicode是一个非常特殊的代码页。第一,它是按文种(Script)编码,而不是按语言、国家或地区编码;第二,它包容了各个代码页的字汇;第三,它在产品平台中,是作为“核心码”或者“轴心码”存在的,无论产品怎样本地化,它都不变;不会像其它代码页那样被“切过来换过去”。
以下列出常用的Code Page以及相应的标示符,以供参考
Value |
Meaning |
CP_ACP |
ANSI code page |
CP_MACCP |
Macintosh code page |
CP_OEMCP |
OEM code page |
CP_SYMBOL |
Symbol code page (42) |
CP_THREAD_ACP |
The current thread's ANSI code page |
CP_UTF7 |
Translate using UTF-7 |
CP_UTF8 |
Translate using UTF-8 |
IGraphBuilder *pGraph;
CoInitialize(NULL);
CoCreateInstance( CLSID_FilterGraph,NULL,CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&pGraph);
IMediaControl *pControl;
IMediaEvent *pEvent;
pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);
char* szFileName;
cin >> szFileName;
USES_CONVERSION
pGraph->RenderFile(T2W(szFileName), NULL);
pControl->Run();
//do the control here
pControl->Release();
pEvent->Release();
pGraph->Release();
CoUninitialize();
#include
void main(void)
{
IGraphBuilder *pGraph = NULL;
IMediaControl *pControl = NULL;
IMediaEvent *pEvent = NULL;
// Initialize the COM library.
HRESULT hr = CoInitialize(NULL);
if (FAILED(hr))
{
printf("ERROR - Could not initialize COM library");
return;
}
// Create the filter graph manager and query for interfaces.
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&pGraph);
if (FAILED(hr))
{
printf("ERROR - Could not create the Filter Graph Manager.");
return;
}
hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);
IBaseFilter* pFilter;
pGraph->AddSourceFilter(L"C://1.avi",L"test",&pFilter);
IPin * pPin;
IEnumPins *pEnum = NULL;
pFilter->EnumPins(&pEnum);
while(pEnum->Next(1,&pPin,NULL)==S_OK)
{
PIN_DIRECTION PinDir;
pPin->QueryDirection(&PinDir);
if(PinDir == PINDIR_OUTPUT)
{
IPin *pTemp;
if(SUCCEEDED(pPin->ConnectedTo(&pTemp)))
{
pTemp->Release();
}
else
{
pGraph->Render(pPin);
}
}
}
if (SUCCEEDED(hr))
{
// Run the graph.
hr = pControl->Run();
if (SUCCEEDED(hr))
{
// Wait for completion.
long evCode;
pEvent->WaitForCompletion(INFINITE, &evCode);
}
}
pControl->Release();
pEvent->Release();
pGraph->Release();
CoUninitialize();
}
DirectShow中并没有提供直接支持媒体文件循环播放的接口方法,因此我们需要捕获事件EC_COMPLETE,在这个事件发生后停止Filter Graph,然后重新定位到媒体文件的开始,从头开始播放。参考代码:
LONG eventCode =0, eventParam1 = 0, eventParam2 = 0;
static BOOL bDone = FALSE;
while(SUCCEEDED(pEvents->GetEvent(&eventCode,&eventParam1,&eventParam2,0))
&&!bDone)
{
pEvents->FreeEventParams(eventCode,eventParam1,eventParam2);
switch(eventCode)
{
case EC_COMPLETE:
case EC_END_OF_SEGMENT:
{
pControl->Stop();
if(SUCCEEDED(pSeeking->SetTimeFormat(&TIME_FORMAT_FRAME)))
{
LONGLONG lFrame,lStart = 0;
pSeeking->GetDuration(&lFrame);
pSeeking->SetPositions(&lStart,AM_SEEKING_AbsolutePositioning|
AM_SEEKING_Segment ,&lFrame,
AM_SEEKING_AbsolutePositioning);
}
if(isLoop)
{
pControl->Run();
bDone = FALSE;
}
break;
}
}
}
所谓时间格式,具体来说是指IMediaSeeking接口进行搜寻时单位的尺度。时间格式表明了进行媒体文件的位置搜寻时究竟应该向后搜寻n帧还是跳过n秒。同样的数字在不同的时间格式下面表示的长短也不相同。具体时间格式的GUID以及描述参考下表:
GUID |
描述 |
TIME_FORMAT_NONE |
无格式 |
TIME_FORMAT_FRAME |
以视频文件的帧为单位(常用) |
TIME_FORMAT_SAMPLE |
以Sample为单位 |
TIME_FORMAT_FIELD |
隔行扫描 |
TIME_FORMAT_BYTE |
对于文件流的字节偏移量。 |
TIME_FORMAT_MEDIA_TIME |
基准时间(常用) |
Windows程序设计
DirectShow开发指南
我的开发笔记
QQ群聊天纪录(~!)
科幻世界(~!!)
http://www.unihan.com.cn/Cjk/Conception.htm
http://eyes4.mblogger.cn/
在本文的写作过程中,本人得到了来自各个方向的支持和帮助,再次表示感谢。
感谢我的偶像Jonh Carmark,让我有了慢条斯理的耐性和勇气。这个世界需要天才来鼓励那些准天才,嘿嘿
感谢LaMothe,让我从一开就清晰地了解COM客户端。
感谢Maxwell公司为我们这些穷人制造的Maxwell速溶咖啡。
感谢科幻世界让我的思路不至于僵死。
感谢腾讯公司的等级制度,让我通宵的时候还能升一下QQ的星星。
感谢重庆大学网络中心为我及时开通了网络。
感谢天,感谢地,感谢命运让我们相遇……………………嘿嘿
欢迎大家去我和老婆的博客:
http://www.blogcn.com/user20/ifdream/index.html
http://www.blogcn.com/user21/inkick/index.html
我的联系方式:
QQ: 78482557
E-Mail:[email protected]
CSDN ID: Inkick(星点)