as ssd-fotoshop软件

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
发布评论