as ssd-fotoshop软件

directshow
2023年4月2日发(作者:电脑知识视频)

DirectShow媒体文件回放总结收藏

作者:Inkick

1.概述

DirectShow中媒体文件回放的过程也就是一个为媒体文件选择相应所需的Filter、构建Filter

Graph、并对FilterGraph的状态进行维持、控制的过程。这里所说的媒体文件,不仅仅是指

音频、视频文件,同时也包括bmp、jpeg、gif等图形图像格式以及midi等数字化音乐序列。

因此,使用DirectShow进行媒体文件的回放需要经过以下的步骤:

2.构建FilterGraph

FilterGraph为Filter提供了一个容器,一个构建完整的FilterGraph也就是一个完整的Filter

连路,这个连路对于程序是透明的,可控制的。而对于每一个媒体文件来说,FilterGraph

与媒体文件存在着对应的关系。也就是说,一个FilterGraph只能实现一个(种)文件的回

放。

在DirectShow中,FilterGraph是由接口对象IGraphBuilder实现的,我们可以调用Win32API

函数CoCreateInstance()建立一个实体。FilterGraph实体建立之后并不具有任何的Filter,因

此不具有任何实际用途。因此我们需要连接需要的Filter来完成FilterGraph的构建。

智能连接这个术语覆盖了一系列FilterGraphManager用于构建所有或部份filtergraph的算

法。任何时候,当FilterGraphManager需要添加filter来完成graph时,它大致做以下几件

事情:

如果有一个filter存在于graph中,而且这个filter有至少一个没有连接的inputpin,FilterGraph

Manager试着去试用这个filter。

否则,FilterGraphManager在已注册的filter中寻找连接时可以接受合适的媒体类型的filter。

每一个filter都注册有一个Merit值,这个值用以标记哪个filter最容易被FilterGraphManager

选中来完成graph。FilterGraphManager按Merit值的顺序来选择filter,Merit值越大,被选

中的机会越大。对于每种流类型(如音频、视频、MIDI),默认的renderer具有一个很高的

Merit值,解码器同样是,专用filter具有低Merit值。

如果FilterGraphManager选择的filter不合适,它会返回来尝试另外的filter组合。

我们有三种构建graph的途径:

graphmanager构建整个graph

graphmanager构建部分graph

3.应用程序构建整个graph

2.1RenderFile

IGraphBuilder提供了多种智能完成FilterGraph构建的方法。最简单的是使用接口方法

IGraphBuilder::RenderFile。

HRESULTRenderFile(LPCWSTRlpwstrFile,LPCWSTRlpwstrPlayList);

第一个参数为文件的路径(祥见后文),第二个参数保留,必须为空。

这个方法需要一个表示媒体文件路径或者URL的Unicode字符串参数。而我们通过界面获

得的文件路径的字符串往往是ANSI字符串。我们可以使用下面方法进行转换:

包含头文件:

#include

#include

这两个头文件包含了ANSI字符串与Unicode字符串相互转化的函数与宏

使用宏:USES_CONVERSION;

定义一个WCHAR的数组:WCHARFileName[MAX_PATH];

而MAX_PATH是在windef.h中定义的:#defineMAX_PATH260

这与windows路径最大字符为260个相符。这个数组保存转化后的Unicode形式表示的路径。

然后可以使用下面的函数进行转换:(假定以ANSI形式给出的字符串为szFile)

wcsncpy(FileName,T2W(szFile),NUMELMS(wFile)-1);

FileName[MAX_PATH-1]=0;

wcsncpy的原型:

wchar_t*wcsncpy(wchar_t*strDest,constwchar_t*strSource,size_tcount);

这个函数的作用类似于strcpy,是实现字符串之间的复制。只不过,这是一个用在Unicode

上的版本。

第一个参数指定了字符串转化后的存放地址,也就是我们要得到的Unicode字符串,第二个

参数指定了要转化的字符串的来源地址,也就是我们要转化的ANSI字符串。在第二个参数

使用了宏T2W(szFile),这个宏可以把一个ANSI字符串转为一个WCHAR类型的字符串。

第三个参数为转化字符串中字符的数量。

现在问题出来了,这个函数的第二个参数需要宽字符串的地址,如果我们有这样的一个地址,

我们还转换什么?因此这个的关键在于T2W上面。让我们来看一下T2W的定义。这个定

义在头文件ATLCONV.H里面(只保留我们比较感兴趣的部分)。

#ifdef_UNICODE

inlineLPWSTRT2W(LPTSTRlp){returnlp;}

inlineLPTSTRW2T(LPWSTRlp){returnlp;}

#else

#defineT2WA2W

#endif

我们可以看到,如果定义了_UNICODE,则T2W直接返回将要转化的字符串,这是因为在

Unicode环境下,ANSI字符拥有和Unicode字符同样的宽度。也就是说,ANSI是Unicode

的一个子集。但是在非Unicode环境下,就将T2W替换成A2W,这样我们返回来看A2W

的定义(这个定义在同样的头文件中):

#defineA2W(lpa)(

((LPCSTR)lpa==NULL)?NULL:(

_convert=(lstrlenA(lpa)+1),

ATLA2WHELPER((LPWSTR)alloca(_convert*2),lpa,_convert)))

宏定义比较晦涩,我们转换成比较好理解的函数形式:

LPCSTRA2W(LPTSTRlpa)

{

if(lpa==NULL)

{

returnNULL;

}

_convert=(lstrlen(A)(lpa)+1);

AtlA2WHelper((LPWSTR)alloca(_covert*2),lpa,_convert);

returnlpa;

}

这个函数的结构比较清晰,首先判断是不是空字符串,如果是空字符串就返回空,因为ANSI

和Unicode意义上的空字符串都是NULL,如果不为空,则开始转换。重点是函数

ATLA2WWHELPER(),这个函数的实现部分在中:

LPWSTRWINAPIAtlA2WHelper(LPWSTRlpw,LPCSTRlpa,intnChars)

{

_ASSERTE(lpa!=NULL);

_ASSERTE(lpw!=NULL);

lpw[0]='0';

MultiByteToWideChar(CP_ACP,0,lpa,-1,lpw,nChars);

returnlpw;

}

这个函数除了做一些必要的安全性判定以及前序准备以外,核心工作是调用了一个函数

MultiByteToWideChar,因此我们还要继续深入。这次我们发现我们找不到源代码了,但是

在MSDN中我们可以得到明确的提示:

intMultiByteToWideChar(

UINTCodePage,//codepage

DWORDdwFlags,//character-typeoptions

LPCSTRlpMultiByteStr,//addressofstringtomap

intcchMultiByte,//numberofbytesinstring

LPWSTRlpWideCharStr,//addressofwide-characterbuffer

intcchWideChar//sizeofbuffer

);

这个API函数接受六个参数(Win32API的风格——参数超多),第一个参数指定了

CodePage(解释见附录5.1),在这里我们可以指定CP_ACP,来表示我们选择ANSICodePage

代表我们要转换的源字符串编码形式为ANSI,第二个参数是一组位标志,决定了如何处理

原字符串中的控制字符或者无效字符。一般指定MB_PRECOMPOSED,第三个参数是源字

符串的地址(或者指针)。第四个字参数是要转化的字符串里面包含了多少个字符,第五个

参数指定了转换后的字符串的存放地址,第六个参数指定了转换后字符串的Buffer的大小,

也就是转换后占用内存空间的多少。

现在我们逐层返回,(如果你已经忘记了我们的初衷是什么建议你听一下F.I.R的歌),根据

最核心的函数MultiByteToWideChar的分析,我们可以得出AtlA2Whelper几个参数的含义:

AtlA2WHelper(LPWSTRlpw,//转化的目标字符串的地址

LPCSTRlpa,//转化的源字符串的地址

intnChars//源字符串中包含的字符数

)

因此在判断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);

returnlpa;

lstrlen函数得到参数字符串的长度(对于ANSI字符串来说是字节数,对于Unicode字符串

来说是字符数),后面的(A)表明参数字符串为ANSI字符串,经过这个参数的调用,_convert

的值变成了要转换的字符串长度加一。为什么要加一呢?因为字符串的结尾要补上一个’

0’,我们总要为这个’0’预留空间。

下一步就是要申请空间来保存转化完成的字符串了,使用(LPWSTR)alloca(_covert*2),分配

字符串数的两倍空间(因为Unicode字符占用的空间是ANSI的两倍),然后将地址转为

LPWSTR,当作参数传递给AtlA2Whelper,开始转换,任务完成!

可是,真的完成了吗?那个该死的_convert是哪里来的?哈哈,还记得我们一开始

说的吗?回忆一下,在使用T2W之前要做的工作是什么?使用宏USES_CONVERSION;!

为什么呢?我们继续看这个宏的定义:

#ifndef_DEBUG

#else

#defineUSES_CONVERSIONint_convert=0

#endif

看到了吗?这个宏其实就是在定义这个_convert变量。因此,当你在不同的作用域中使用

T2W宏的时候,一定要在使用前写上这个东东。

闭上眼睛,整理一下思路,所有技术文档上面几行说清楚的RenderFile,被我洋洋

洒洒写了将近三页。中间涉及的相关内容完全可以写成一份技术白皮书。如果这两页能够说

清楚什么的话,我还是很有成就感的。如果对Unicode以及ANSI感兴趣,请自行查阅相关

的资料,我发誓这辈子都不想碰到类似的问题了。

实例代码:见附录5.2,RenderFile

2.2Render

回到我们亲爱的DirectShow(来个拥抱吧!)。

这个接口方法从一个输出Pin开始,自动构建剩余的FilterGraph。这个方法自动添加必要的

Filter,直到添加到一个RenderFilter为止。

HRESULTRender(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:yourprocesshere

}

第一行代码首先声明了一个枚举器指针,然后第二行调用Filter上的接口方法生成一个实例,

接着是用一个While循环开始枚举,每循环一次,pPin指针指向下一个Pin,然后针对每一

个Pin,进行以下处理:

2,判断每一个Pin的连接状态和连接方向。

判断Pin的连接方向:

PIN_DIRECTIONPinDir;

pPin->QueryDirection(&PinDir);

//TODO:testthedirectionhere

第一行代码定义了一个类型为PIN_DIRECTION的变量PinDir,然后在第二行调用接口方法

pPin->QueryDirection(&PinDir);调用后,PinDir的值就成为了pPin上的方向。然后根据实际

需要进行判断处理。

判断Pin的连接状态:

IPin*pTemp;

If(SUCCEEDED(pPin->ConnectedTo(&pTemp)))

{

//itisnotthejustpinwewant,becauseitishasbeenconnected

}

else

{

//thispiniswewant

}

判断Pin的连接状态我们使用了接口方法pPin->ConnectedTo(&pTemp),这个接口方法的真

正用途是返回和这个Pin连接的另一个Pin的指针。如果没有其他的Pin和这个Pin连接,

就返回E_FAIL,因此我们可以使用SUCCEEDED宏来捕获接口方法的返回值,以判断Pin

的连接状态。

在获得某个Filter上没有连接而且方向符合数据流向的Pin之后,我们就可以调用这个Render

接口方法完成这个FilterGraph的构架。

一般来说,这个接口方法最好配合IGraphBuilder::AddSourceFilter使用,也就是首先加入一

个源Filter,然后从这个源Filter的输出Pin开始,完成FilterGraph的构架。

AddSourceFilter方法添加一个能render特定文件的sourcefilter。首先,它依据协议名、文件

扩展名、或文件头在已注册的filter中寻找匹配的那个。如果此方法定位到了一个合适的

sourcefilter,它便立刻创建一个这个filter的实例,并将其添加到graph中,然后调用filter

的IFileSourceFilter::Load方法。

实例代码:见附录5.4

2.3ConnectDriect

与RenderFile以及Render等处于FilterGraph的全局地位的完成方式相比,IgraphBuilder

也提供了多种面向局部,具体而微的接口方法。这些接口方法大多是针对两个Filter或者两

个Pin之间的连接。ConnectDirect方法就是其中之一。这个方法尝试将两个Filter直接相连,

如果不成功,直接返回。不作其他处理。这个接口方法我们一般用作两个Pin之间的协商检

查,而不直接用于两个Filter的连接。原因就在于这个函数成功率太低,如果用于两个Filter

的连接需要反复尝试不同的Filter,将严重影响效率。当然,如果程序可以明确两个Filter

可以连接,使用这个接口方法可以使用一种简洁的方法完成连接。

HRESULTConnectDirect(IPin*ppinOut,IPin*ppinIn,constAM_MEDIA_TYPE*pmt);

前两个参数是要连接的两个Filter的输入输出Pin,最后一个是媒体类型的结构,可

以为空。关于媒体结构,请参考附录5.2。

2.4Connect

这个接口方法是ConnectDirect接口方法的延伸和扩展。该接口方法同样是连接两个Filter,

但是与ConnectDirect方法不同的是,这个接口方法首先尝试ConnectDirect方法连接两个

Filter,如果不能直接连接(这种情况多数由于媒体类型不匹配),则尝试使用中间Filter连

接。因此,这个接口方法的执行成功率较高。

2.5AddFilter

这个接口方法将指定的Filter加入到FilterGraph中,但这个新加入的Filter并不会和Filter

Graph中的其他Filter形成连接。与此类似,将FilterGraph中的某个Filter与其他Filter连

接的断开,并不会使之脱离FilterGraph,要是Filter脱离FilterGraph,必须要在断开连接之

后,调用RemoveFilter接口方法。

2.6手动构建FilterGraph

手工构建FilterGraph的基础在于对于媒体文件所需要的Filter有着相当深入清晰地

了解的基础上。否则,手工构架的FilterGraph将存在可靠性、健壮性等问题。

手动构架FilterGraph主要用到上面介绍的几个接口方法:AddFilter、Connect、以

及(很少情况)ConnectDirect。这几个接口方法在上文没有详细说明的原因也是因为如此,

放在这里做一个系统的交代。

主要思路如下:

n根据文件类型或者设定的FilterGraph选择Filter

选择Filter主要通过枚举系统存在、支持的Filter,并根据预先的设定或者在用户参与的条

件下使用用户所选择的Filter

n将选中的Filter加入到FilterGraph中

这一步主要使用上面的几个接口方法实现,具体参看附带的实例代码。

n如果连接过程中出现问题,尝试加入中间Filter,或者替换Filter。

n完成连接。

我们不推荐整个FilterGraph均由手工构建,一种既能充分利用DirectShow智能化又能体现

FilterGraph的针对性的方法是,使用AddSourceFilter加入源Filter,然后开始手工连接(当

然,这种方法不适用需要使用非标准非默认文件源Filter的FilterGraph)。

实例代码(暂时空缺):

3.对FilterGraph的控制

对FilterGraph的控制包括对FilterGraph的状态的控制,对媒体文件视频、音频的控制等。

下面分三部分介绍:

3.1对FilterGraph状态的控制

在介绍对FilterGraph状态的控制之前,首先要说明的是FilterGraph的状态。FilterGraph

在任何时候只有三种状态:运行、暂停、停止。这三种状态的标明了媒体文件回放时的状态。

当刚刚为一个媒体文件构建一个FilterGraph时,这个FilterGraph处于停止状态,随着文件

的播放,FilterGraph也转入运行状态(事实上是FilterGraph转入运行状态导致文件开始播

放)。当FilterGraph进入暂停状态文件的播放也随之暂停。当文件播放完毕时,FilterGraph

重新回到停止状态。(这段描述只是为了表明文件播放的状态和FilterGraph的状态的对应关

系,不能正确地反映两者相互作用相互影响的顺序和关系)

因此,我们对FilterGraph状态的控制也就集中在三种状态之间的切换。

对于FilterGraph状态的控制我们需要使用另外一个接口:IMediaControl,这个接口的实现

在IFilterGraph上,但是应用程序使用时,要在IGraphBuilder上申请这个接口。

pGraph->QureyInterface(QueryInterface(IID_IMediaControl,(void**)&pControl);

获得这个接口后,我们就可以使用上面的接口方法(SDK文档上面这个接口有九个接口方

法,但是我们只能用前五个,后四个是为VB实现的,VB真好!):

IMediaControl::Run();

IMediaControl::Pause();

IMediaControl::Stop();

IMediaControl::GetState(LONGmsTimeout,OAFilterState*pfs);

IMediaControl::StopWhenReady();

这个五个接口方法中,前三个都是自解释的,而且没有任何参数,返回值是标准的HRESULT,

这种函数的调用让人觉得呼吸顺畅,神清气爽。可是我们不是在森林里做有氧运动,事实上,

通宵写这个文档已经让我喘不过气来了。

让我们集中精力:

第四个接口方法看起来不是那么痛苦,返回FilterGraph的状态。需要解释的是参数。第一

个参数指定了一个时间段,DirectShow里面FilterGraph状态的转化并不一定是同步的,因

此当你调用这个接口方法的时候,FilterGraph可能正在向一种新的状态转化,因此这个接

口方法将等待至FilterGraph稳定到某一个状态或者等待的时候超过了第一个参数指定的那

个时间段。不过这种不稳定的状态实在太少了(我们是在使用DirectShow而不是在研究高

能物理电子轨道的跃迁),因此这个参数放心的写上0,微软设置这个参数的目的——我猜

——是为了提高程序的可靠性,即便是这样微软还是有那么多的Bug,谁知道呢?第二个参

数是一个指向OAFilterState的指针,这个参数的值在方法成功后成为FilterGraph的状态。

第五个接口方法就相当让人迷茫了,虽然它的命名很好J。这个接口方法的暂停Filter

Graph,等待数据处理完毕后停止FilterGraph。这个接口方法主要用途并不是在于对Filter

Graph状态的控制,而是在于当FilterGraph停止时提供了一种有效的搜寻方法。具体的原

理我们与IMediaSeeking接口一起介绍。

完了吗?真的完了吗?没错,但这只是一部分,只是应用程序对FilterGraph进行控制的一

部分,如果FilterGraph需要与应用程序进行交互呢?

FilterGraph与应用程序之间事件的交互主要通过另外的一个接口IMediaEventEx来实现。

(FilterGraph上有三个接口与事件处理有关,IMediaEventSink,用于Filter与FilterGraph

之间的事件交互,IMediaEvent与IMediaEventEx是应用程序与FilterGraph进行事件交互的

接口,因为后者继承自前者,是前者的扩展,因此我们大多选用后者。)

IMediaEventEx接口与IMediaControl接口类似,实现在IFilterGraph上,但应用程序应该在

IGrahpBuilder上申请。

IMediaEventEx*pEvent;

pGraph->QureyInterface(IID_IMediaEventEx,(void**)&pEvent);

FilterGraph与应用程序的事件交互有两种方式,这两种方式的区别在于事件交互的主动者

不同。一种是FilterGraph主动将事件的发生通知给应用程序(窗体),一种是FilterGraph

维持一个消息泵,应用程序不断的从中取出消息。在实际开发中,我们一般选中第一种,第

二种方法一般用于不适合消息传递和没有窗体的场合。

首先我们自定义一个消息:

#defineWM_DS_EVENT19850415

后面的那个整数是我随便加的(其实也不是随便啦,你送我礼物我就告诉你),只要保证不

与Windows内部消息冲突即可。

然后设置GraphFilter的事件通知码,设置之后,当FilterGraph中某个事件发生后FilterGraph

就会将事件通知码以Windows消息的方式发给程序窗体,窗体捕捉到这个消息后,就可以

调用相应的接口方法,来获知具体的事件。

pEvent->SetNotifyWindow((OAHWND)hwnd,WM_DS_EVENT,0);

这个接口方法注册一个窗口,这个窗口在FilterGraph有事件发生时成为消息的接收者,第

二个参数为一个long类型,即事件发生的的消息。最后一个参数是作为lParam而接受的,

这里直接给了0。因为我们只需要知道FilterGraph有事件发生了,而不需要知道更多的信

息。

现在开始处理,在WinProc函数(我喜欢Win32的重要理由就是我不用写消息映射宏,我

是一个懒人啊!)捕获WM_DS_EVENT消息:

caseWM_DS_EVEVT:

{

if(pEvent)

{

LONGeventCode=0,wParam=0,lParam=0;

while(SUCCEEDED(pEvent->GetEvent(&eventCode,&wParam.&lParam,0)))

{

pEvent->FreeEventParams(eventCode,);

swtich(eventCode)

{

//processeventshere

}

}

}

}

这段代码调用了IMediaEventEx接口方法:GetEvent和FreeEventParams,关于这两个接口

方法的说明如下:

HRESULTGetEvent(long*lEventCode,long*lParam1,long*lParam2,longmsTimeout);

GetEvent是从DirectShowFilterGraph的消息泵中取出一条消息,这个接口方法成功后,第

一个参数表明了事件消息,也就是发生了什么事件。第二个第三个参数表明了一些辅助信息,

根据事件的不同含义也有所不同。最后一个参数的含义与IMediaControl::GetStates接口方法

的事件参数类似,给0表示方法立即返回。

HRESULTFreeEventParams(longlEventCode,longlParam1,longlParam2);

这个接口方法释放GetEvent调用时为参数分配的空间,注意,只是回收空间,参数的值没

有变化,因此我们建议在调用GetEvent之后应该立即调用这个接口方法,以免造成资源的

浪费,甚至内存泄漏。

经过这些之后,eventCode就代表了事件的类型。然后随便怎么处理吧。在DirectShow中常

见的事件有:

nEC_COMPLETE表示媒体文件已经回放完毕。但此时FilterGraph并不直接转入

停止状态,需要我们调用IMediaControl::Stop()方法。而且FilterGraph没有提供给我们循环

播放一个文件的方法,因此我们需要对这个事件进行处理。参加附录5.3

nEC_ERRORABOR表示FilterGraph运行出错

nEC_USERABORT表示用户中止了文件的回放

nEC_REPAINT表示视频窗口IVideoWindows需要重画当前帧。

除此以外,IMeidaEventEx接口上还有其他接口方法。

IMediaEventEx上的接口方法:

nHRESULTSetNotifyFlags(longlNoNotifyFlags);设置事件通知码。

nHRESULTGetNotifyFlags(long*lplNoNotifyFlags);得到事件通知码。

IMediaEvent上的接口方法:

nHRESULTCancelDefaultHandling(longlEvCode);为指定的消息取消默认的消息

处理。在取消后,这条消息将传递给应用程序,必须由应用程序手动处理。

nRestoreDefaultHandling(longlEvCode)为指定的消息恢复默认的消息处理

nWaitForCompletion(longmsTimeout,long*pEvCode);FilterGraph挂起,直到文件

回放完毕。

到目前为止,对于文件回放的FilterGraph控制与交互的接口涉及到IMediaControl和

IMediaEventEx,这两个接口相互配合,完成了应用程序与FilterGraph的协调合作。

现在的问题放在媒体文件的搜寻上,我谨慎的认为这一部分应该属于对FilterGraph的状态

的控制,而并非对媒体文件视频或者音频的控制。让我们使用程序员的语言吧,这总是让人

觉得很美妙。

对于媒体文件的搜寻,DirectShow提供了另外的一个接口IMediaSeeking,(数一下,我们已

经遇到了多少个接口?)这个接口的方法众多,不过对于文件回放来说,我们似乎只需要用

到不多的几个,但谁能保证其余的就不会用到呢?

与时间相关的接口方法:

nHRESULTGetCapabilities(DWORD*pCapabilities);这个接口方法返回当前媒体

文件类型是否支持搜寻。参数在接口方法调用后成为当前FilterGraph的搜寻能力的标示。

这个指针指向一个AM_SEEKING_SEEKING_CAPABILITIES类型的标志。这个标志为一组

枚举值,含义自解释。

nHRESULTCheckCapabilities(DWORD*pCapabilities);这个接口方法将检查参数

所代表的搜寻是否被支持。接口方法返回时,参数指针内容中将是支持的搜寻能力。

nHRESULTIsFormatSupported(constGUID*pFormat);检查参数所代表的时间格

式是否支持,如果支持返回S_O。时间格式参见附录5.6

nHRESULTQueryPreferredFormat(GUID*pFormat);返回首选的时间格式。

nHRESULTGetTimeFormat(GUID*pFormat);返回当前的时间格式

nHRESULTIsUsingTimeFormat(constGUID*pFormat);返回是否使用参数表示的

时间格式。如果是返回S_OK。

nHRESULTSetTimeFormat(constGUID*pFormat);设置当前使用的时间格式。

nHRESULTConvertTimeFormat(LONGLONG*pTarget,constGUID*pTargetFormat,

LONGLONGSource,constGUID*pSourceFormat);将一种时间格式下的时间值转为另一种

时间格式下的时间值。第一个参数保存转换后时间值的存放的地址。第二个参数以及第四个

参数分别指定了目标格式和源格式。第三个参数指定了源时间值。

关于搜寻范围的接口方法:

nHRESULTGetDuration(LONGLONG*pDuration);返回文件的长度,以当前时间格

式为单位

nHRESULTGetStopPosition(LONGLONG*pStop);返回停止的位置。以当前时间格

式为单位。

nHRESULTGetCurrentPosition(LONGLONG*pCurrent)返回当前位置,当前时间格

式为单位。

nHRESULTGetPositions(LONGLONG*pCurrent,LONGLONG*pStop);返回当前位

置和停止位置,以当前时间为单位。

nHRESULTSetPositions(LONGLONG*pCurrent,DWORDdwCurrentFlags,

LONGLONG*pStop,DWORDdwStopFlags);设置停止位置,当前位置。以当前时间格式为单

位。

nHRESULTGetAvailable(LONGLONG*pEarliest,LONGLONG*pLatest);设置有效

的搜寻范围,第一个参数为范围的开始,第二个参数为范围的结束。以当前的时间格式为单

位。

关于播放速率的接口方法:

nHRESULTSetRate(doubledRate);取得播放速率,正常速率为1

nHRESULTGetRate(double*dRate);设置播放速率

这些接口配合使用基本上实现了对媒体文件的搜寻,在开发中应该根据实际情况灵活运用,

不要绝对的使用某一种时间格式或者接口方法。

现在返回来让我们看一个历史遗留问题,我都差点忘了。如果你也忘了,跳过这里。嘿嘿。

IMediaControl::StopWhenReady();这个接口方法主要用于当FilterGraph停止时对媒体文件

的搜寻。FilterGraph一旦停止,对于当前位置的改变VideoWindow不会自动重绘,因此,

对IMediaSeeking::SetPositions接口方法的调用不会更新VideoWindow。因此如果要在完成

对媒体文件的搜寻之后显示一帧新帧,需要调用这个接口方法。这个方法首先将FilterGraph

转入暂停状态,然后等待操作完成后重新将FilterGraph转入停止状态。在暂停状态的时候

数据在仍然在graph中传递,因此VideoRender将不断地播放新帧。

3.2对媒体文件视频的控制

终于开始了一个新的话题,而且这个话题比起上一个涉及了3个接口的话题要轻松许多,虽

然也涉及了两个接口和很多接口方法,但是要具体很多。让我用台灯烤一下手,然后开始。

视频控制包含两个内容:一个是对于视频流的控制,一个是对于视频窗口的控制。对于视频

流的控制我们更关心数据的格式、走向、有效等信息,而对于视频窗口而言,我们则更关注

与视频文件播放的尺寸、位置等外观因素。

对于视频流的控制我们使用IBasicVideo接口,IBasicVideo接口继承自IDispatch,这个接口

的实现在VideoRender上,但是应用程序要在FilterGraph上申请这个接口。和以前一样,

这样申请一个接口:

pGraph->QueryInterface(IID_IBasicAudio,(void**)&pBasicAudio);

在说明这个接口上的接口方法之前,我们首先明确几个必要的概念:

源矩形:源矩形是指要被显示的原始图像的一部分

目标矩形:目标矩形是指VideoWindows接受源矩形的一部分

视频矩形:视频矩形是指原始的视频图像。

也就是说,VideoRender捕获原始图像形成源矩形,然后将源矩形拉伸或者压缩使之符合目

标矩形的大小。所有矩形的单位都是像素。

这三个矩形中,视频矩形由媒体文件决定,而其他的两个则可以由我们根据实际需要程序设

置,IBasicVideo提供了如下方法对源矩形和目标矩形进行控制,这些方法都是自解释的。

nget_/put_DestinationHeight

nget_/put_DestinationLeft

nget_/put_DestinationTop

nget_/put_DestinationWidth

nget_/put_SourceHeight

nget_/put_SourceLeft

nget_/put_SourceTop

nget_/put_SourceWidth

nget_VideoHeight

nget_VideoWidth

nSetSourcePosition

nSetDestinationPosition

除此以外,IBasicVideo接口提供了与媒体文件视频属性密切相关的接口方法:

nget_AvgTimePerFrame得到每一帧的平均时间

nget_BitErrorRate得到位出错率。这里的出错是指进行编码校验出错。

nget_BitRate得到位速率,单位bps

另外一个比较重要的方法是得到当前帧:

GetCurrentImage(long*pBufferSize,long*pDIBImage);第一个参数为图片的大小,可以根据

视频矩形的大小、位深加上BMP文件头信息的大小计算出来,第二个参数在方法返回时指

向一个位图的信息。

IBasicVideo给我们提供了较为底层和深入的控制视频流的方法,尽管如此,我们实际开发

中使用IBasicVideo的机会并不多。而对于与播放显示视频流的窗口息息相关的

IVideoWindow才是我们关注的重点。

IVideoWindow在Graph以及VideoRender上都有实现,但应用程序应该在Filter

Graph上申请这个接口。

pGraph->QueryInterface(IID_IVideoWindow,(void**)&pVideoWindow);

具体部分参看SDK文档。

3.3对媒体文件音频的控制

这是我们最后一个话题了,对于声音的控制。你无法让你的同伴闭上嘴,但是你可以随意设

置你的媒体文件音量的大小、声道的平衡。一个优秀的程序员在程序中可以获得上帝一样的

权利。

相对视频流的控制,音频流的控制则要简单明确的多。首先是我们使用的接口:IBasicAudio。

这个接口和往常一样在AudioRender上实现,但是应用程序需要在FilterGraph上申请这个

接口。

接口方法以及功能说明如下:

nHRESULTget_Balance(long*plBalance);得到当前音频流声道的平衡度。接口方法

调用成功后,参数指针的引用的值为一个范围从-10000到+10000的长整数。这个值的含义

是:-10000表示右声道无声,+10000表示左声道无声,0表示声道平衡。

nHRESULTput_Balance(longlBalance);设置当前音频流的声道平衡度。参数的含

义以及取值参考上一个接口方法的说明。

nHRESULTget_Volume(long*plVolume)得到音频流的音量,接口方法调用成功后,

参数指针的引用的值除以100得到声音音强值,单位dB。

nHRESULTput_Volume(longlVolume);设置音频流的音量。参数的含义和取值参考

上一个接口方法的说明。

好了,随着声音动听而流畅的播放,我们的回放也就完成了,就是这么简单吗?但愿吧。

4,释放FilterGraph

对于FilterGraph的释放来说,其实并不应该列为一个主题。但是考虑到这是一个程序的收

尾工作而且对于一个安全可靠的程序是一个至关重要的保证,同时为了保证这个文件的结构

符合DiretShow媒体文件回放的步骤,才单独将其列为一个主题。

释放FilterGraph要按照以下步骤:

清空FilterGraph(不必须,建议)

释放由FilterGraph上申请的接口,将接口指针置为空

释放FilterGraph,同样将接口指针置为空。

调用CoUninitialize();

5.DirectShow文件回放接口关系图

根据上述分析,我们可以得到用于媒体文件回放的DirectShowFilterGraph的接口关系图:

说明:

AudioRender上实现了IBasciAudio接口,VideoRender上实现了IBasicVideo以及

IVideoWindow,但是这些接口都应该从FilterGraph上申请。图中的标示方法只是为了作用

关系,不代表实际的接口情况。

6.附录

本附录中收集了一些相关的资料,有些来源于网上,有些是我自己的代码,有些是SDK文

档的翻译整理。

6.1关于CodePage

所谓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也是一种CodePage,其代码页ID=1200。但在严格意义上,

UCS/Unicode是一个非常特殊的代码页。第一,它是按文种(Script)编码,而不是按语言、国

家或地区编码;第二,它包容了各个代码页的字汇;第三,它在产品平台中,是作为“核心

码”或者“轴心码”存在的,无论产品怎样本地化,它都不变;不会像其它代码页那样被“切

过来换过去”。

以下列出常用的CodePage以及相应的标示符,以供参考

Value

Meaning

CP_ACP

ANSIcodepage

CP_MACCP

Macintoshcodepage

CP_OEMCP

OEMcodepage

CP_SYMBOL

Symbolcodepage(42)

CP_THREAD_ACP

Thecurrentthread'sANSIcodepage

CP_UTF7

TranslateusingUTF-7

CP_UTF8

TranslateusingUTF-8

6.2RenderFile示例代码:

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();

//dothecontrolhere

pControl->Release();

pEvent->Release();

pGraph->Release();

CoUninitialize();

6.3媒体类型

6.4Render示例代码

#include

voidmain(void)

{

IGraphBuilder*pGraph=NULL;

IMediaControl*pControl=NULL;

IMediaEvent*pEvent=NULL;

//InitializetheCOMlibrary.

HRESULThr=CoInitialize(NULL);

if(FAILED(hr))

{

printf("ERROR-CouldnotinitializeCOMlibrary");

return;

}

//Createthefiltergraphmanagerandqueryforinterfaces.

hr=CoCreateInstance(CLSID_FilterGraph,NULL,CLSCTX_INPROC_SERVER,

IID_IGraphBuilder,(void**)&pGraph);

if(FAILED(hr))

{

printf("ERROR-CouldnotcreatetheFilterGraphManager.");

return;

}

hr=pGraph->QueryInterface(IID_IMediaControl,(void**)&pControl);

hr=pGraph->QueryInterface(IID_IMediaEvent,(void**)&pEvent);

IBaseFilter*pFilter;

pGraph->AddSourceFilter(L"C:",L"test",&pFilter);

IPin*pPin;

IEnumPins*pEnum=NULL;

pFilter->EnumPins(&pEnum);

while(pEnum->Next(1,&pPin,NULL)==S_OK)

{

PIN_DIRECTIONPinDir;

pPin->QueryDirection(&PinDir);

if(PinDir==PINDIR_OUTPUT)

{

IPin*pTemp;

if(SUCCEEDED(pPin->ConnectedTo(&pTemp)))

{

pTemp->Release();

}

else

{

pGraph->Render(pPin);

}

}

}

if(SUCCEEDED(hr))

{

//Runthegraph.

hr=pControl->Run();

if(SUCCEEDED(hr))

{

//Waitforcompletion.

longevCode;

pEvent->WaitForCompletion(INFINITE,&evCode);

}

}

pControl->Release();

pEvent->Release();

pGraph->Release();

CoUninitialize();

}

6.5关于文件循环播放

DirectShow中并没有提供直接支持媒体文件循环播放的接口方法,因此我们需要捕获事件

EC_COMPLETE,在这个事件发生后停止FilterGraph,然后重新定位到媒体文件的开始,

从头开始播放。参考代码:

LONGeventCode=0,eventParam1=0,eventParam2=0;

staticBOOLbDone=FALSE;

while(SUCCEEDED(pEvents->GetEvent(&eventCode,&eventParam1,&eventParam2,0))

&&!bDone)

{

pEvents->FreeEventParams(eventCode,eventParam1,eventParam2);

switch(eventCode)

{

caseEC_COMPLETE:

caseEC_END_OF_SEGMENT:

{

pControl->Stop();

if(SUCCEEDED(pSeeking->SetTimeFormat(&TIME_FORMAT_FRAME)))

{

LONGLONGlFrame,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;

}

}

}

6.6时间格式

所谓时间格式,具体来说是指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

基准时间(常用)

7.参考文献:

Windows程序设计

DirectShow开发指南

我的开发笔记

QQ群聊天纪录(~!)

科幻世界(~!!)

/Cjk/

/

8.致谢

在本文的写作过程中,本人得到了来自各个方向的支持和帮助,再次表示感谢。

感谢我的偶像JonhCarmark,让我有了慢条斯理的耐性和勇气。这个世界需要天才来鼓励那

些准天才,嘿嘿

感谢LaMothe,让我从一开就清晰地了解COM客户端。

感谢Maxwell公司为我们这些穷人制造的Maxwell速溶咖啡。

感谢科幻世界让我的思路不至于僵死。

感谢腾讯公司的等级制度,让我通宵的时候还能升一下QQ的星星。

感谢重庆大学网络中心为我及时开通了网络。

感谢天,感谢地,感谢命运让我们相遇„„„„„„„„嘿嘿

本文来自CSDN博客,转载请标明出处:

/benny5609/archive/2008/04/27/

更多推荐

directshow