DirectShow 媒体文件回放总结
MU小组文档
作者:Inkick
本文99%内容原创,转载请注明出处
1.概述
DirectShow中媒体文件回放的过程也就是一个为媒体文件选择相应所需的Filter、构建Filter Graph、并对Filter Graph的状态进行维持、控制的过程。这里所说的媒体文件,不仅仅是指音频、视频文件,同时也包括bmp、jpeg、gif等图形图像格式以及midi等数字化音乐序列。
因此,使用DirectShow进行媒体文件的回放需要经过以下的步骤:
2.构建Filter Graph
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
2.1 RenderFile
IGraphBuilder提供了多种智能完成FilterGraph构建的方法。最简单的是使用接口方法IGraphBuilder::RenderFile。
HRESULT RenderFile(LPCWSTR lpwstrFile, LPCWSTR lpwstrPlayList);
第一个参数为文件的路径(祥见后文),第二个参数保留,必须为空。
这个方法需要一个表示媒体文件路径或者URL的Unicode字符串参数。而我们通过界面获得的文件路径的字符串往往是ANSI字符串。我们可以使用下面方法进行转换:
包含头文件:
#include <tchar.h>
#include <atlbase.h>
这两个头文件包含了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,开始转换,任务完成!