精华内容
参与话题
问答
  • 最简单的基于DirectShow的示例:视频播放器

    万次阅读 热门讨论 2015-01-10 18:29:47
    本文记录一个最简单的基于DirectShow的视频播放器。DirectShow是一个庞大的框架,可以在Windows下实现多种多样的视频处理需求。但是它的“庞大”也使得新手不太容易学习它的使用。本文的例子正是为解决这一问题而做...

    =====================================================

    最简单的基于DirectShow的示例文章列表:

    最简单的基于DirectShow的示例:视频播放器

    最简单的基于DirectShow的示例:视频播放器图形界面版

    最简单的基于DirectShow的示例:视频播放器自定义版

    最简单的基于DirectShow的示例:获取Filter信息

    =====================================================


    本文记录一个最简单的基于DirectShow的视频播放器。DirectShow是一个庞大的框架,可以在Windows下实现多种多样的视频处理需求。但是它的“庞大”也使得新手不太容易学习它的使用。本文的例子正是为解决这一问题而做的,它只包含了使用DirectShow播放一个视频文件所需要的最重要的函数。


    流程图

    最简单的使用DirectShow播放视频文件的流程如下图所示。


    流程图中涉及到几个接口如下所示。
    IGraphBuilder:继承自IFilterGraph,用于构建Filter Graph。相比于IFilterGraph来说IGraphBuilder提供了一些更加“智能”的方法,例如RenderFile()方法。
    IMediaControl:提供和播放控制有关的一些接口。

    IMediaEvent:用来处理Filter Graph发出的事件。


    流程图中关键函数的作用如下所示。
    CoInitialize() :初始化COM运行环境。
    CoCreateInstance(…,pGraph) :用指定的类标识符创建一个Com对象。在该播放器中类标识符为“CLSID_FilterGraph”,用于创建IGraphBuilder。
    pGraph->QueryInterface(…,pControl) :通过QueryInterface()查询某个组件是否支持某个特定的接口。在这里查询IMediaControl接口。
    pGraph->QueryInterface(…,pEvent) :同上。在这里查询IMediaEvent接口。
    pGraph->RenderFile("xxx.mkv"):为指定的文件智能的构建一个Filter Graph。
    pControl->Run() :开始运行Filter Graph中的所有Filter。
    pEvent->WaitForCompletion() :等待Filter Graph处理完所有数据。

    CoUninitialize():释放CoInitialize()初始化的COM运行环境。

    注意上述几个函数是构建一个基于DirectShow的视频播放器所必须的函数,除了上述几个接口之外还经常用到以下几个接口:

    IBasicVideo:提供和视频有关的一些接口。
    IBasicAudio:提供和音频有关的一些接口。
    IVideoWindow:提供和窗口有关的一些接口。
    IMediaSeeking:提供和播放位置有关的一些接口。

    源代码

    /**
     * 最简单的基于DirectShow的视频播放器
     * Simplest DirectShow Player
     *
     * 雷霄骅 Lei Xiaohua
     * leixiaohua1020@126.com
     * 中国传媒大学/数字电视技术
     * Communication University of China / Digital TV Technology
     * http://blog.csdn.net/leixiaohua1020
     *
     * 本程序是一个最简单的基于DirectShow的播放器。
     * 适合初学者学习DirectShow。
     *
     * This example is the simplest Player based on DirectShow.
     * Suitable for the beginner of DirectShow.
     */
    
    #include "stdafx.h"
    
    #include <dshow.h>
    #include <atlconv.h>
    
    
    #define OUTPUT_INFO 1
    
    //Show Filter in FilterGpragh
    int show_filters_in_filtergraph(IGraphBuilder *pGraph){
    	printf("Filters in FilterGpragh=======\n");
    	USES_CONVERSION;
    	IEnumFilters *pFilterEnum=NULL;
    	if(FAILED(pGraph->EnumFilters(&pFilterEnum))){
    		pFilterEnum->Release();
    		return -1;
    	}
    	pFilterEnum->Reset();
    	IBaseFilter * filter = NULL;
    	ULONG fetchCount = 0;
    	//Pin Info
    	while (SUCCEEDED(pFilterEnum->Next(1, &filter, &fetchCount)) && fetchCount){
    		if (!filter){
    			continue;
    		}
    		FILTER_INFO FilterInfo;
    		if (FAILED(filter->QueryFilterInfo(&FilterInfo))){
    			continue;
    		}
    		printf("[%s]\n",W2A(FilterInfo.achName));
    		filter->Release();
    	}
    	pFilterEnum->Release();
    	printf("==============================\n");
    	return 0;
    }
    
    
    int _tmain(int argc, _TCHAR* argv[])
    {
    	IGraphBuilder *pGraph = NULL;
        IMediaControl *pControl = NULL;
        IMediaEvent   *pEvent = NULL; 
    	//Get some param--------------
    	HRESULT hr1;
    	IBasicVideo *pVideo=NULL;
    	IBasicAudio *pAudio=NULL;
    	IVideoWindow *pWindow=NULL;
    	IMediaSeeking *pSeeking=NULL;
    	
    	
        // Init COM
        HRESULT hr = CoInitialize(NULL);
        if (FAILED(hr)){
            printf("Error - Can't init COM.");
            return -1;
        }
    
    	// Create FilterGraph
       hr=CoCreateInstance(CLSID_FilterGraph, NULL,CLSCTX_INPROC_SERVER,IID_IGraphBuilder, (void **)&pGraph);
        if (FAILED(hr)){
            printf("Error - Can't create Filter Graph.");
            return -1;
        }
       //  Query Interface
        hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
        hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);
    	// RenderFile
        hr = pGraph->RenderFile(L"cuc_ieschool.mov", NULL);
    	if (FAILED(hr)){
    		printf("Error - Can't Render File.");
    		return -1;
    	}
    #if OUTPUT_INFO
    	//Get some information----------
    	long video_w=0,video_h=0,video_bitrate=0,audio_volume=0;
    	long long duration_1=0,position_1=0;
    	REFTIME avgtimeperframe=0;
    	float framerate=0,duration_sec=0,progress=0,position_sec=0;
    	//Video
    	hr1=pGraph->QueryInterface(IID_IBasicVideo, (void **)&pVideo);
    	pVideo->get_VideoWidth(&video_w);
    	pVideo->get_VideoHeight(&video_h);
    	pVideo->get_AvgTimePerFrame(&avgtimeperframe);
    	framerate=1/avgtimeperframe;
    	//pVideo->get_BitRate(&video_bitrate);
    	//Audio
    	hr1=pGraph->QueryInterface(IID_IBasicAudio, (void **)&pAudio);
    	//Mute
    	//pAudio->put_Volume(-10000);
    	printf("Some Information:\n");
    	printf("Video Resolution:\t%dx%d\n",video_w,video_h);
    	printf("Video Framerate:\t%.3f\n",framerate);
    	//Window
    	hr1=pGraph->QueryInterface(IID_IVideoWindow, (void **)&pWindow);
    	pWindow->put_Caption(L"Simplest DirectShow Player");
    	//pWindow->put_Width(480);
    	//pWindow->put_Height(272);
    	//Seek
    	hr1=pGraph->QueryInterface(IID_IMediaSeeking, (void **)&pSeeking);
    	pSeeking->GetDuration(&duration_1);
    	//time unit:100ns=0.0000001s
    	duration_sec=(float)duration_1/10000000.0;
    	printf("Duration:\t%.2f s\n",duration_sec);
    	//pSeeking->SetPositions();
    	//PlayBack Rate
    	//pSeeking->SetRate(2.0);
    
    	//Show Filter in FilterGpagh
    	show_filters_in_filtergraph(pGraph);
    	//----------------------
    #endif
    
    	printf("Progress Info\n");
    	printf("Position\tProgress\n");
        if (SUCCEEDED(hr)){
            // Run
            hr = pControl->Run();
            if (SUCCEEDED(hr)){
    			long evCode=0;
    			//pEvent->WaitForCompletion(INFINITE, &evCode);
    			while(evCode!=EC_COMPLETE){
    				//Info
    #if OUTPUT_INFO
    				pSeeking->GetCurrentPosition(&position_1);
    				position_sec=(float)position_1/10000000.0;
    				progress=position_sec*100/duration_sec;
    				printf("%7.2fs\t%5.2f%%\n",position_sec,progress);
    #endif
    				//1000ms
    				pEvent->WaitForCompletion(1000, &evCode);
    			}
            }
        }
    	// Release resource
        pControl->Release();
        pEvent->Release();
        pGraph->Release();
        CoUninitialize();
    	return 0;
    }
    

    运行结果

    程序运行后即可开始播放一个“cuc_ieschool.mov”文件。程序运行时候的截图如下所示。由图可见运行的同时程序在控制台中打印出了两种信息:
    (1) 该视频的相关信息

    (2) 播放该视频的 Filter Graph中的Filter(该功能通过函数show_filters_in_filtergraph()完成)。


    可以通过定义在代码最前面宏OUTPUT_INFO控制是否输出视频的信息。定义成“0”的话则不会输出视频的信息。如下所示。
    #define OUTPUT_INFO 1

    下载


    Simplest DirectShow Example


    项目主页

    SourceForge:https://sourceforge.net/projects/simplestdirectshowexample/

    Github:https://github.com/leixiaohua1020/simplest_directshow_example

    开源中国:http://git.oschina.net/leixiaohua1020/simplest_directshow_example


    CDSN下载地址:http://download.csdn.net/detail/leixiaohua1020/8348163

    本程序包含了DirectShow开发的示例程序。适合DirectShow初学者进行学习。
    它包含了以下几个子程序:
    simplest_directshow_player: 最简单的基于DirectShow的视频播放器。
    simplest_directshow_player_custom: 最简单的基于DirectShow的视频播放器(Custom)。
    playerGUI: 最简单的基于DirectShow的播放器-图形界面版。
    simplest_directshow_info: 最简单的Directshow信息显示例子。
    simplest_directshow_filter: 目前还未完成。


    展开全文
  • Directshow完整介绍

    万次阅读 2011-12-12 15:08:48
    最近一段时间,在编写DirectShow应用程序时常常遇到一些问题,原因是对DirectShow技术没有较全面地掌握,对各个接口间的关系以及filter与filter之间连接的内部过程等都只是一知半解,除了再仔细地看看DirectShow的...

    最近一段时间,在编写DirectShow应用程序时常常遇到一些问题,原因是对DirectShow技术没有较全面地掌握,对各个接口间的关系以及filter与filter之间连接的内部过程等都只是一知半解,除了再仔细地看看DirectShow的基类库源文件之外,觉得也很有必要从头到尾看一遍DirectShow的MSDN文档。在看时顺便有选择地翻译出来,一来以便以后再看时可以轻松点,二来也敦促自己不能不求甚解早早看看了事。在翻译的过程中也加了一些自己的补充,因为觉得某些MSDN章节实在是过于简单还有些模棱两可。

    1. DirectShow介绍 
        DirectShow是一个 windows平台上的流媒体框架,提供了高质量的多媒体流采集和回放功能。它支持多种多样的媒体文件格式,包括ASF、MPEG、AVI、MP3和 WAV文件,同时支持使用WDM驱动或早期的VFW驱动来进行多媒体流的采集。DirectShow整合了其它的DirectX技术,能自动地侦测并使用可利用的音视频硬件加速,也能支持没有硬件加速的系统。 
        DirectShow大大简化了媒体回放、格式转换和采集工作。但与此同时,它也为用户自定义的解决方案提供了底层流控制框架,从而使用户可以自行创建支持新的文件格式或其它用途的DirectShow组件。 
        以下是几个使用DirectShow编写的典型应用:DVD播放器、视频编辑应用、AVI到ASF转换器、MP3播放器和数字视频采集应用。 
        DirectShow 是建立在组件对象模型(COM)上的,因此当你编写DirectShow应用时,你必须具备COM客户端程序编写的知识。对于大部分的应用,你不需要实现自己的COM对象,DirectShow提供了大部分你需要的DirectShow组件,但是假如你需要编写自己的DirectShow组件,你还需要具备编写COM组件的知识。 
    1.1. DirectShow支持的格式 
        DirectShow是一个开放的框架,因此只要有合适的filter来分析和解码,它可以支持任何格式。DirectShow默认支持以下的文件类型和压缩格式: 
        注:打*号的需要Windows Media Format SDK支持 
        文件类型: 
          Windows Media? Audio (WMA)* 
          Windows Media? Video (WMV)* 
          Advanced Systems Format (ASF)* 
          Motion Picture Experts Group (MPEG) 
          Audio-Video Interleaved (AVI) 
          QuickTime (version 2 and lower) 
          WAV 
          AIFF 
          AU 
          SND 
          MIDI 
        压缩格式: 
          Windows Media Video* 
          ISO MPEG-4 video version 1.0* 
          Microsoft MPEG-4 version 3* 
          Sipro Labs ACELP* 
          Windows Media Audio* 
          MPEG Audio Layer-3 (MP3) (decompression only) 
          Digital Video (DV) 
          MPEG-1 (decompression only) 
          MJPEG 
          Cinepak 
        微软自己没有提供MPEG2解码器,一些可用的DirectShow MPEG2硬件或软件解码器是由第三方提供的。 
    1.2. 常见问题集(摘录) 
    1.2.1. 一般问题
     
    *DirectShow支持哪些操作系统? 
          DirectShow 支持Windows9X、Windows2000、Windows Me和Windows XP。 
    *使用 DirectShow需要多少COM知识? 
          应用程序开发者只需要基本的COM组件知识:实例化COM组件、调用接口、管理接口的引用计数。Filter开发者则需要更多。 
    *有与DirectShow兼容的硬件列表(HCL)吗? 
          没有。如果硬件兼容DirectShow,DirectShow会使用它们,如果没有兼容的硬件,DirectShow使用GDI绘制视频,以及使用 WaveOut系列多媒体API来播放音频。 
    *可以使用哪些语言来编写DirectShow应用? 
          DirectShow 主要为C/C++开发设计。Visual Basic只能使用其中的很小一部分。可以通过MS JScript或VB Script来支持基于脚本的DVD和TV应用。也可能用Delphi来编写,但SDK文档不提供这方面的内容。 
    *DirectShow 会通过托管代码实现吗? 
          目前还没有这个计划。DirectX SDK提供了有限的使用音视频回放类的托管回放功能,你可以使用COM interop创建托管代码的DirectShow客户端应用,但是因为性能上的原因,不推荐创建运行在CLR上的filter。

    *DirectShow开发需要什么样的编译器 ? 
          任何能够产生COM对象的编译器都可以。 
    *DirectShow和DirectX的其它组件的关系 
          DirectShow 和DirectX的其它组件在内部进行联系。DirectShow在硬件的支持下使用DirectSound和DirectDraw。Video Renderer和Overlay Mixer使用DirectDraw 3和DirectDraw5表面(surfaces)。Video Mixing Renderer 7(只支持WINXP)使用DirectDraw7表面。Video Mixing Renderer 9使用最新的(目前是Directx9)Direct3D API函数。即便是某个应用程序包含了DirectX其它组件,你也不必使用其它组件的API去编写它。参考SDK的例子:Texture3D Sample。 
    *DirectShow与ActiveMovie的关系? 
          ActiveMovie 是DirectShow原来的名称,现已不再使用,但是一部分API仍保留了"AM"的前缀,比如AM_MEDIA_TYPE和 IAMVideoAccelerator。 
    *DirectShow是限于多媒体应用吗? 
          DirectShow 默认包含的组件主要是为音视频流设计的,但是,DirectShow框架已经成功地用于其它数据流的解决方案中。 
    *GraphEdit 工具有源码吗?GraphEdit.exe是否可再发布? 
          没有源码,不可再发布。 
    *DMO 可以代替DirectShow filter吗? 
          在编写编码器、解码器、效果器应用时,鼓励用DMO代替 DirectShow filter。在其它的应用中,使用DirectShow filter可能会比较合适。 
    1.2.2. 程序编写问题 
    *如何设置编译环境,需要哪些头文件和库? 
          参考"设置编译环境"章节 
    *GraphEdit列示了很多没有文档支持的filter,它们都是些什么? 
          GraphEdit 枚举了所有作为filter类型注册在系统中的filter,包括由第三方应用程序安装的filter,以及其它微软技术如Windows Media或NetMeeting安装的,另外,一些DirectShow filter被用来做硬编码或硬解码驱动的外壳。Microsoft H.263 Video Codec用于NetMeeting,不再被DirectShow支持。 
    *如何知道 DirectShow已经被安装? 
          调用CoCreateInstance创建一个Filter Graph Manager实例,如果成功,表示DirectShow已经被安装,下面是一个例子:

          IGraphBuilder *pGraph;

           HRESULT hr = CoCreateInstance(CLSID_FilterGraph, 
                 NULL, CLSCTX_INPROC_SERVER, 
                 IID_IGraphBuilder, (void **) &pGraph);

    *如果不通过属性设置页来更改filter的设置? 
          当然是通过filter提供的接口罗。如果没有提供,就没有办法啦 
    *DirectShow能通知应用程序当前回放位置吗? 
          不提供回调来通知位置,需要使用一个计时器定时调用 IMediaSeeking::GetCurrentPosition方法来得到当前回放位置。 
    *filter 运行在哪个特权级别下? 
          运行在Ring 3特权级别下,某些流控制驱动(如音视频采集驱动)运行在Ring 0特权级别下。 
    *需要一个Kernel调试器吗? 
          这依据具体的项目。安装DirectX调试运行时库(DirectX debug runtime library)意味着安装调试驱动(Debug driver)和其它核心组件(kernel mode component),因此如果你的应用程序在其中的某个组件中产生了一个调试断言(debug assert),你的机器就会自动重启除非你拥有一个kernal调试器。 
    *DEFINE_GUID宏是怎么工作的? 
          使用DEFINE_GUID宏可以让你通过包含同一个头文件来定义GUID值而不必使用extern关键词。比如,你的工程中有三个源文件:src1.cpp,src2.cpp,src3.cpp,它们都使用一个相同的GUID值,而为了保证一致性,这个GUID只能在你的工程中定义一次,这时,其它的源文件必须定义外部引用来使用它。用了DEFINE_GUID,你可以使用在所有源文件中包含同一个头文件,在头文件中这样定义GUID:

        DEFINE_GUID(CLSID_MyObject, 
             0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);

                这个例子中GUID为0,实际编程中请用Guidgen工具来产生一个GUID,在其中的一个源文件中,在你的头文件前包含initguid.h,如:

      // Src1.cpp
    #include 
    #include "MyGuids.h"
    
    // Src2.cpp
    #include "MyGuids.h" 
    

    // Src3.cpp #include "MyGuids.h"

    在没有包含Initguid.h的地方,DEFINE_GUID宏创建外部引用来使用GUID值,在包含Initguid.h的地方,DEFINE_GUID重定义DEFINE_GUID宏以产生GUID的定义。 
    如是没有在任何地方添加 Initguid.h,你会得到一个链接错误:"unresolved external symbol." ,如果同样的GUID包含Initguid.h两次,会得到编译错误"redefinition; multiple initialization."要解决这些问题,请确认Initguid.h只包含一次。同样的,不要包含Initguid.h到预编译头文件中去,因为预编译头文件会被每个源文件包含。

    [此贴子已经被作者于 2005-11-26 2:49:14编辑过]


    --  作者:admin 
    --  发布时间:2005-11-26 2:46:00 
    --   
    桃花坞里桃花庵,桃花庵下桃花仙。 
    桃花仙人种桃树,又摘桃花换酒钱。 
    酒醒只在花前坐,酒醉还来花下眠。 
    半醉半醒日复日,花落花开年复年。 
    但愿老死花酒间,不愿鞠躬车马前。 
    车尘马足显者事,酒盏花枝隐士缘。 
    若将显者比隐士,一在平地一在天。 
    若将花酒比车马,彼何碌碌我何闲。 
    别人笑我太疯癫,我笑他人看不穿。 
    不见五陵豪杰墓,无花无酒锄作田。


    --  作者:admin 
    --  发布时间:2005-11-26 2:48:00 
    --  开始DirectShow旅程

    2. 开始DirectShow旅程 
        这个章节的内容主要是编写 DirectShow应用所需的一些基本概念,可以把它当作一个高级介绍,理解这些内容只需具备一般的编程和有关多媒体的知识。 
    2.1. 设置DirectShow开发的编译环境 
        这节内容描述了如何来编译DirectShow应用。你可以使用命令行形式来编译一个工程,也可以在Microsoft Visual Studio集成环境下(包含VC++)实现。 
        头文件: 
        所有的DirectShow应用都需要Dshow.h这个头文件,某些DirectShow接口需要附加的头文件,参考接口的说明视具体情况定。 
        库文件: 
        DirectShow使用以下库文件: 
        Strmiids.lib 输出类标识(CLSID)和接口标识(IID),所有DirectShow应用均需此库。 
        Quartz.lib   输出AMGetErrorText函数,如果不调用此函数,此库不是必需的。 
        有了以上这些头文件和库文件,你已经可以编写 DirectShow应用了,但是微软建议使用DirectShow基类库来编写filter,这样可以大大减少程序编写的工作量。要使用 DirectShow基类库,需要先编译它,基类库位于SDK的Samples//Multimedia//DirectShow //BaseClasses文件夹下,包含两个版本的库:发布版(retail version)Strmbase.lib和调试版(debug version)Strmbasd.lib。具体参见"创建DirectShow Filter"一节。 
    2.2. DirectShow应用程序编程简介 
        这节介绍DirectShow用到的一些基本术语和概念,看完这节后,你将能够编写你的第一个DirectShow应用程序。 
    Filter和Filter Graph 
        一个DirectShow应用程序是由一个个称为filter 的软件构件组合而成的,filter执行一些多媒体流的操作,如:读文件、从视频采集设备中获得视频、将不同的格式的流解码如MPEG1、将数据送到图形卡或声卡中去。 
        Filter 接收输入并产生输出。举个例子,一个解码MPEG1视频流的filter,输入MPEG1格式的视频流,输出一系列未压缩的视频帧。 
        在 DirectShow中,应用程序要实现功能就必须将这些filter链接在一起,因而一个filter的输出就变成了另一个filter的输入。这一系列串在一起的filter称为filter graph 。例如,下图就显示了一个播放avi文件的 filter graph: 
     
        File Source(Async) filter从硬盘中读取avi文件;AVI Splitter filter分析文件并将其分解成两个流:一个压缩的视频流和一个音频流;AVI Decompressor filter将视频帧解码,Video Renderer filter将解码后的视频帧通过DirectDraw或GDI显示出来;Default DirectSound Device filter使用DirectSound播放音频流。 
        应用程序没有必要对这些数据流进行管理,而是通过一个叫Filter Graph Manager这个上层组件来控制这些filter。应用程序调用上层API如"Run"(通过graph移动数据)或"Stop"(停止移动数据)。如果你需要对数据流作更多的操作,你可以通过COM接口直接进入filter。Filter Graph Manager同样也输出事件通知给应用程序。 
        Filter Graph的另一个用途是将filter连在一起创建一个filter graph。 
        编写一个DirectShow应用程序大体需要三个步骤: 
        1.创建一个Filter Graph Manager的实例 
        2.使用Filter Graph Manager创建一个filter graph,此时,需要已经具备所有必需的filter。 
        3.使用Filter Graph Manager控制filter graph和通过这些filter的流,在这个过程中,应用程序会收到Filter Graph Manager发送的事件。 
        完成这些后,应用程序需发布这个Filter Graph Manager和所有的filter。 
    2.3. 播放一个文件 
        这一章以本节这个有趣的例子来结束,这个例子是一个播放音频或视频文件的简单控制台程序。程序只有寥寥数行,但却展示了DirectShow编程的强大能力。 
        正如上一节所讲的创建DirectShow应用程序的三个步骤,第一步,首先,需要调用CoInitialize来作初始化,然后调用CoCreateInstance创建Filter Graph Manager:

        HRESULT hr = CoInitialize(NULL); 
        if (FAILED(hr)) 
        { 
            return; 
        }

        IGraphBuilder *pGraph; 
        HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, 
            CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&pGraph);

        如上所示,类标识符(CLSID)是CLSID_FilterGraph。Filter Graph Manager由进程内DLL(in-process DLL)提供,因此参数3,dwClsContext的值为CLSCTX_INPROC_SERVER。由于DirectShow运行自由线程模式 (free-threading model),所以你同样可以使用COINIT_MULTITHREADED参数来调用CoInitializeEx。 
        第二步是创建filter graph,调用CoCreateInstance得到的IGraphBuilder接口包含了大部分创建filter graph的方法。在这个例子中还需要另外两个接口:IMediaControl和IMediaEvent。 
        IMediaControl 控制数据流,它包含开启和停止graph的方法;IMediaEvent包含从Filter Graph Manager获取事件的方法,在这个例子中,这个接口用来得到回放结束事件。 
        所有这些接口由Filter Graph Manager提供,使用得到的IGraphBuiler接口指针来查询得到。

        IMediaControl *pControl; 
        IMediaEvent   *pEvent; 
        hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl); 
        hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

        现在你可以创建filter graph了,对于文件回放只需要一个简单的调用:

       hr = pGraph->RenderFile(L"C:Example.avi", NULL);

        IGraphBuilder::RenderFile方法创建了一个能够播放指定文件的 filter graph,事实上,原本需要做的一些如创建filter实例及将这些filter连接起来的工作,都由这个方法自动完成了,如果是视频文件,这个 filter graph看起来应该是这个样子: 
        [file source]->[如果是缩格式,这里是个解码器]->[Video Renderer] 
        要开始回放,调用 IMediaControl::Run方法:

          hr = pControl->Run();

        当filter graph运行时,数据经过各个filter最后回放为视频或音频。回放发生在一个单独的线程中。你可以通过调用 IMediaEvent::WaitForCompletion方法来等待回放的结束:

        long evCode = 0; 
        pEvent->WaitForCompletion(INFINITE, &evCode);

        这个方法在播放期间被阻塞,直至播放结束或超时。 
        当应用程序结束时,需要释放接口指针并关闭COM库:

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

        // Build the graph. IMPORTANT: Change this string to a file on your system. 
        hr = pGraph->RenderFile(L"C:Example.avi", NULL); 
        if (SUCCEEDED(hr)) 
        { 
            // Run the graph. 
            hr = pControl->Run(); 
            if (SUCCEEDED(hr)) 
            { 
                // Wait for completion. 
                long evCode; 
                pEvent->WaitForCompletion(INFINITE, &evCode);

                // Note: Do not use INFINITE in a real application, because it 
                // can block indefinitely. 
            } 
        } 
        pControl->Release(); 
        pEvent->Release(); 
        pGraph->Release(); 
        CoUninitialize(); 
    }


    --  作者:admin 
    --  发布时间:2005-11-26 2:49:00 
    --  3. 关于DirectShow

    3.1. DirectShow体系概述 
        多媒体的难题 
        处理多媒体有几个主要的难题: 
        *多媒体流包含了巨大的数据量,而这些数据都必须非常快地被处理 
        *音频和视频必须同步,因此它们必须在同一时间开始或停止,并以同一速率播放 
        *数据可能来自很多的源,如本地文件、网络、电视广播和视频摄像机 
        *数据有各种各样的格式,如 AVI、ASF、MPEG和DV 
        *程序员无法预知最终用户使用什么样的硬件设备 
        DirectShow的解决方案 
        DirectShow 被设计成用来解决所有这些难题,它主要的设计目的就是通过将复杂的数据转输、硬件的多样性和同步问题从应用程序中独立出来,从而简化在windows平台上数字媒体应用程序的开发任务。 
        要实现数据高效地被处理,需要流化音视频数据,而DirectShow会尽可能地使用 DirectDraw和DirectSound,从而高效地将数据送到用户的声音和图形设备中进行播放。同步则是通过在媒体数据中加入时间戳来实现。而 DirectShow模块化的架构,使其可以轻松操纵变化多端的源、格式和硬件设备,在这样的架构里,应用程序只需组合和匹配多个filter来实现功能。 
        DirectShow提供的filter支持基于WDM的采集和调谐设备,也支持早先的VFW采集卡和为ACM和VCM接口编写的编码器。 
        下图显示了应用程序、DirectShow组件和DirectShow支持的硬件和软件组件之间的关系: 

        如图,DirectShow将应用程序与众多复杂的设备隔离开来,通信和控制这些设备均出DirectShow的 filter来完成。DirectShow同样为某种文件格式提供与之对应的编解码器。


    --  作者:admin 
    --  发布时间:2005-11-26 2:50:00 
    -- 

    3.2. Filter Graph和它的组件 
        这一节描述了 DirectShow的主要组件,为DirectShow应用程序和DirectShow Filter开发者提供一个介绍。应用程序开发者可以忽略掉很多底层部分,但是,了解底层对于理解DirectShow架构还是很有帮助的。 
    3.2.1. 关于DirectShow Filter 
        DirectShow使用一个模块化的架构,每个处理过程都由一个叫做filter 的COM对象来实现。DirectShow为应用程序提供了一系列标准的filter,开发者也可以编写自己的filter来扩展DirectShow的功能。下面是播放一个AVI文件的各个步骤: 
        *从文件中读取数据并转成字节流(File Source filter) 
        *检查AVI头,分析字节流并将它们分离成视频和音频(AVI Aplitter filter) 
        *将视频解码(不同的解码filter,取决于不同的压缩格式) 
        *将视频显示出来(Video Renderer filter) 
        *将音频送入声卡(Default DirectSound Device filter)

        如图所示,每个filter与一个或多个其它的filter相连,其中的连接点也是一个COM对象,称作Pin ,filter使用Pin将数据从一个filter转移到另一个,图中的箭头指示了数据流动的方向。在DirectShow中,这一系列连接在一起的filter称作filter graph。 
        Filter可能处于有三种不同的状态:运行、停止和暂停状态。filter在运行状态时处理数据,停止状态时停止处理数据,暂停状态则是表示就绪,可以开始进入运行状态。除了极个别的情况,一个filter Graph中的所有filter通常都处理同一个状态下,因此,filter graph也可以称其处于运行、停止、暂停状态。 
        Filter 可以被分成几个大的种类: 
    *source filter - filter graph的数据源,这些数据可以来自文件、网络、摄像头或任何其它东西。每一个source filter操纵不同类型的数据源。 
    *transform filter - 接收数据,处理数据并将它送入下一个filter。编码filter和解码filter都属于这个种类。 
    *Renderer filter - 处于filter链的未端,接受数据并将其展现给用户。比如,一个视频renderer在显示器上绘制视频图像;一个音频renderer将音频数据送入声卡;一个写文件filter(file-writer filter)将数据存盘。 
    *splitter filter - 分析输入的数据流并将其分解成两路或多路,比如,AVI splitter分析字节流并将其分解成视频流和音频流。 
    *mux filter - 将多路输入流合并成一路。比如,AVI Mux正好与AVI splitter做相反的工作,它将视频和音频流合成为一个AVI格式的字节流。 
        以上的分类并不是绝对的,比如,ASF Reader Filter同时充当了source filter和splitter filter的角色。 
        所有的DirectShow filter都提供IBaseFilter接口,所有的Pin也都提供IPin接口。DirectShow也定义了许多其它的接口以实现特定的功能。

    3.2.2. 关于Filter Graph Manager 
    Filter Graph Manager
     是一个用以控制 filter graph中的filter的COM对象。它提供了许多功能,包括: 
        *协调filter之间的状态变化 
        *建立参考时钟(reference clock) 
        *将事件返回给应用程序 
        *提供应用程序建立filter graph的方法 
        这里先简单地描述一个这些功能。 
    状态变化: filter们的状态变化必须遵照一个特定的次序,因此,应用程序不能将状态变化的命令直接发给filter,而是将一个简单的命令发给filter graph manager,由它来将命令分发给各个filter。定位命令同样使用这种方式,应用程序发送一个定位命令给filter graph manager,由它来分发。 
    参考时钟: 在filter graph中的所有filter都使用一个相同的时钟,称为参考时钟(reference clock)。参考时钟保证了所有流的同步。一个视频帧或一段音频样本被播放的时间钞称作呈现时间(presentation time)。呈现时间精确地相对于参考时钟。Filter Graph Manager通常选择的参考时钟是声卡参考时钟或系统时钟。 
    Graph 事件: filter graph manager使用一个消息队列来通知应用程序发生在filter graph中的事件。 
    Graph-buliding 方法: filter graph manager提供给应用程序将filter加入到filter graph中的方法,以及将filter与filter连接或断开连接的方法。 
        Filter graph manager不提供操纵在filter之间流动数据的功能,这个功能由filter通过pin连接在一个单独的线程中自行完成。 
    3.2.3. 关于媒体类型(Media Type) 
        因为DirectShow是模块化的,因此需要有一个在filter graph各个点之间描述格式的方法。比如说,AVI回放,数据输入时是一个RIFF块的流,然后被分解成视频和音频流。视频流由一个个可能被压缩的视频帧组成,解压后,视频流又变成了一系列未压缩的位图。音频与视频类似。 
    Media Type:DirectShow怎样来描述格式     Media Type是描述数字媒体格式的常用方式。当两个filter连接时,它们需要协商决定同一个Media Type。Media Type标识了从上一个filter递交到下一个filter或物理层的数据流格式。如果两个filter对Media Type不能协商一致,则不能连接。 
        对于某些应用程序,你不必去关心Media type,比如文件回放,DirectShow做了所有有关它的事情。 
        Media type使用AM_MEDIA_TYPE结构体来定义,这个结构体包含了以下内容: 
    *Major type: 主类型,是一个GUID,定义了数据的整体类型,包括了:视频、音频、未分析的字节流、MIDI等。 
    *Subtype: 子类型,另一个GUID,进一步定义了数据格式。比如,如果主类型是视频,则子类型可以是RGB-24、RGB-32、UYVY等格式,如果主类型是音频,则可能是PCM或MPEG-1 payload等。子类型提供了比主类型更多的内容,但仍未提供完整的格式定义,比如,子类型没有定义图像尺寸和帧率,这些都将在Format block中被定义。 
    *Format block: 格式块,定义了具体的格式。格式块是 AM_MEDIA_TYPE结构体中一个单独被分配的内存空间,pbFormat成员指向这块内存空间。因为不同的格式会有不同的格式描述,所以 pbFormat成员的类型是void*。比如,PCM音频使用WAVEFORMATEX结构体,视频使用不同的结构体包括:VIDEOINFOHEADER和VIDEOINFOHEADER2。formattype成员是一个GUID,指定了格式块包含了哪种结构体,每一种格式的结构体都被分配了GUID。cbFormat成员定义了格式式块的长度。 
        当格式块被定义时,主类型和子类型包含的信息就显得有点多余了。其实,主类型和子类型为识别格式提供了一个便利的方法,比方说,你可以指定一个普通的24位RGB格式(MEDIASUBTYPE_RGB24),而不需去关心VIDEOINFOHEADER结构体中诸如图像尺寸和帧率这些信息。 
        下面是一个filter检查媒体类型的例子:

    HRESULT CheckMediaType(AM_MEDIA_TYPE *pmt) 

        if (pmt == NULL) return E_POINTER;

        // 检查主类型,我们需要的是视频 
        if (pmt->majortype != MEDIATYPE_Video) 
        { 
            return VFW_E_INVALIDMEDIATYPE; 
        }

        // 检查子类型,我们需要的是24-bit RGB. 
        if (pmt->subtype != MEDIASUBTYPE_RGB24) 
        { 
            return VFW_E_INVALIDMEDIATYPE; 
        }

        // 检查format type和格式块的大小. 
        if ((pmt->formattype == FORMAT_VideoInfo) && 
             (pmt->cbFormat >= sizeof(VIDEOINFOHEADER) && 
             (pmt->pbFormat != NULL)) 
        { 
            // 现在可以安全地将格式块指针指向正确的结构体。 
            VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat; 
            // 检查pVIH (未展示). 如果正确,返回S_OK. 
            return S_OK; 
        }

        return VFW_E_INVALIDMEDIATYPE; 
    }

        AM_MEDIA_TYPE结构体还包含了一些任选项,用来提供附加的信息,filter不需要这些信息: 
    *ISampleSize, 如果这个字段非零,表示这是每个sample的尺寸,如果是零,则表示sample的尺寸会改变。 
    *bFixdSizeSamples ,如果这个布尔类型的标记是TRUE,表示ISampleSize有效,否则,你可以忽略ISampleSize。 
    *bTemporalCompression ,如果这个布尔类型的标记是FALSE,表示所有帧都是关键帧。

    3.2.4. 关于媒体样本(Media Sample)和分配器(Allocator) 
        Filter 通过Pin与Pin之间的连接来递交数据,数据从一个filter的输出Pin转移到另一个filter的输入Pin,除了个别情况,实现这种功能通常的方法是调用输入Pin上的IMemInputPin::Receive方法。 
        依靠filter,媒体数据的内存空间可以通过多个途径来分配:在堆上、在DirectDraw表面(surface)、在共享GDI内存或使用其它的分配机制。这个负责分配内存空间的对象称为分配器(Allocator),是一个暴露 IMemAllocator接口的COM对象。 
        当两个Pin相连时,其中的一个Pin必须提供一个分配器。DirectShow定义了一个方法调用序列来决定到底由哪个Pin来提供分配器。Pin还负责协商分配器创建的缓冲数和每个缓冲的尺寸。 
        在数据流开始之前,分配器创建了一个缓冲池。在数据流动过程中,上游filter在缓冲中填入数据并递送给下游filter,但是,上游filter递送给下游filter的并不是原始的缓冲区指针,而是一个称为媒体样本(Media Sample)的COM对象,它由分配器创建并用来管理缓冲区,暴露IMediaSample接口。一个媒体样本包含: 
        *指向下层缓冲区的指针 
        *时间戳 
        *各种标记 
        *可选的媒体类型 
        时间戳定义了呈现时间(presentation time),用以让renderer filter确定播放的合适时机。各种标记可以用来指示很多事情,比如,数据在上一个sample后是否被打段过(如重新定位、掉帧)等。媒体类型为流中间改变数据格式提供了途径,通常,没有媒体类型的sample,被认为从上一个sample以来数据格式没有被改变过。 
        当filter使用一个缓冲时,它保存了sample上的参考计数。分配器使用参考计数来决定什么时候可以重用这个缓冲,这防止了一个filter在写一个缓冲时另一个 filter还在使用这个缓冲,除非所有的filter都释放了这个缓冲,否则sample不会将其返回给分配器的缓冲池。 
    3.2.5. 硬件如何参与Filter Graph 
        这一节描述了DirectShow如何与音频和视频硬件交互。 
    外壳filter(Wrapper Filter) 
        所有的DirectShow filter都是用户模式的软件组件。为了使象视频采集卡这样的内核模式的硬件驱动加入到filter graph中,必须使其象用户模式的filter那样。DirectShow提供外壳filter来完成这个功能,这类filter包括:Audio Capture filter、VFW Capture filter、TV Tuner filter、TV Audio filter和Analog Video Crossbar filter。DirectShow也提供一个叫KsProxy的filter,它可以实现任何类型的WDM流驱动。硬件商通过提供一个Ksproxy plug-in来扩展KsProxy,以使其支持自己的功能,ksproxy plug-in是一个被KsProxy聚合的COM对象。 
        外壳filter通过暴露COM接口来实现设备的功能。应用程序使用这些接口将信息传递给filter,filter再把这些COM调用转化为设备驱动调用,将信息传递到内核模式下的设备中去,然后返回结果给应用程序。TV Tuner、TV Audio、Analog Video Crossbar和KsProxy filter都通过IKsPropertySet接口来支持驱动的自定义属性,VFW Capture filter和Audio Capture filter不支持这种方式。 
        外壳filter使应用程序可以象控制其它 directshow filter一样来控制设备,filter已经封装了与内核驱动通信的细节。 
    Video for Windows Devices 
        VFW Capture filter支持早期的VFW采集卡,当一个设备加入到目标系统中支后,它可以被directshow使用系统设备枚举器(System Device Enumerator)发现并加入到filter graph中去。 
        音频采集(Audio Capture)和混音设备(声卡)(Mixing Device/Sound Card) 
        较新的声卡都有麦克风等设备的插口,而且大多数这类声卡都有板级的混频能力,可单独控制每一个连接设备的音量及高低音。在directshow中,声卡的输入和混频设备被Audio Capture filter封装。每个声卡都能被系统设备枚举器发现。要查看你的系统中的所有声卡,只需打开GraphEdit,从Audio Capture Sources一类中选择即可,每个在这个类里的filter都是一个单独的Audio Capture filter。

    WDM流设备     较新的硬解码设备和采集卡都遵照WDM规范。这些设备和比VFW设备更强大的功能,以及可以应用于多种系统(winxp,winNT,win2000,win98/me)。WDM视频采集卡支持许多VFW所没有的功能,包括枚举采集的格式、编程控制视频参数(如对比度、亮度)、编程选择输入端和电视调谐支持。 
        为了支持WDM流设备,directshow提供了KsProxy filter(ksproxy.ax)。KsProxy被称为“瑞士军刀",因为它可以做很多不同的事情。filter上pin的数量,以及COM接口的数量,取决于底层驱动的能力。KsProxy不以"KsProxy"这个名字显示在filter graph中,而是使用一个已在注册表中登记的设备名称。要查看你系统中的WDM设备,可以运行GraphEdit然后从WDM Streaming这个类别中选择。即使你的系统中只有一块WDM卡,这块卡也可能包含多个设备,而每一个设备都表现为一个filter,每个 filter是实际意义上的KsProxy。 
        应用程序使用系统设备枚举器在系统中寻找WDM设备moniker,然后调用moniker 的BindToObject来实例化。因为KsProxy能够表现所有类型的WDM设备,因此它必须通过询问驱动来决定哪些属性是驱动所支持的。属性集是一组数据结构的集合,被WDM设备使用,也被诸如MPEG2软解码filter这样的用户模式filter使用。KsProxy通过暴露COM接口来配置自己,硬件商则通过提供插件来扩展KsProxy,插件暴露硬件商自定义的一些接口,用以实现特殊的功能。所有这些细节对于应用程序来说都是不可见的,应用程序通过KsProxy控制设备就象控制其它的DirectShow filter一样。 
    内核流 
        WDM设备支持内核流,在内核流中数据在内核模式下被彻底流化而永远不需要切换到用户模式下去,从而避免了在内核模式和用户模式之间切换的巨大开销,内核流允许高的比特率而不消耗CPU的时间。基于 WDM的filter能够使用内核流将多媒体数据一个硬件设备送入到另一个中去,既可以是在同一块卡中也可以在不同的卡中,而不需要将数据拷入系统主存。 
        从应用程序的视点来看,数据好象是从一个用户模式的filter传到另一个中去,但是实际上,数据根本就没有传到用户模式下过,而是可能支接从内核模式的设备中传到下一个中去直至被呈现(render)在显卡上。某些情况,比如采集视频到一个文件中去,在某些点上需要将数据从内核模式传入到用户模式,但是,仍然没有必要将数据拷贝到内存的一个新位置中去。 
        应用程序开发者通常只需了解一个内核流的背景知识而不需要深究它的细节。


    --  作者:admin 
    --  发布时间:2005-11-26 2:51:00 
    -- 

    3.3. 构建Filter Graph 
    3.3.1. 用于构建Graph的组件
     
        DirectShow 提供了一系列用于构建filter graph的组件,包括: 
    *Filter Graph Manager 。这个对象用于控制filter graph,支持IGraphBuilder、IMediaControl和IMediaEventEx等许多接口。所有的directshow应用程序都需要在某些地方用到这个对象,虽然在有些情况下,是其它的对象为应用程序创建了filter graph manager。 
    *Capture Graph Builder 。这个对象为构建filter graph提供附加的方法。它最初是为构建提供视频采集的graph而设计的(这正是它的名字由来),但是对于构建许多另外类型的filter graph也是很有用的。它支持ICaptureGraphBuilder2接口。 
    *Filter Mapper和System Device Enumerator 。这些对象用于查找在系统中注册的或代表硬件驱动的filter。 
    *DVD Graph Builder 。这个对象构建用以回放和导航DVD的filter graph。它支持IDvdGraphBuilder接口。基于脚本的应用程序能够使用MSWebDVD ActiveX控件来控制DVD回放。 
    *Video Control 。WinXP提供这个ActiveX控件,用于操纵directshow中的数据和模拟电视。 
    智能连接(Intelligent Connect) 
        智能连接这个术语覆盖了一系列Filter Graph Manager用于构建所有或部份filter graph的算法。任何时候,当Filter Graph Manager需要添加filter来完成graph时,它大致做以下几件事情: 
        1.如果有一个filter存在于 graph中,而且这个filter有至少一个没有连接的input pin,Filter Graph Manager试着去试用这个filter。 
        2. 否则,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组合。

    3.3.2 Grap构建概述 
        创建一个filter graph,从创建一个Filter Graph Manager实例开始:

         IGraphBuilder* pIGB; 
         HRESULT hr = CoCreateInstance(CLSID_FilterGraph, 
         NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder,(void **)&pIGB);

    Filter Graph Manager支持下列Graph构建方法: 
    *IFilterGraph::ConnectDirect
     ,在两个pin之间进行直接连接,如果连接失败,则返回失败 
    *IFilterGraph::Connect ,连接两个Pin,如果可能的话,直接连接它们,否则,在中间加入其它的filter来完成连接。 
    *IGraphBuilder::Render ,从某个输出Pin处开始完成余下的graph构建。该方法会自动在输出pin后面添加必须的filter,直到renderer filter为止。 
    *IGraphBuilder::RenderFile ,构建一个完整的文件回放graph。 
    *IGraphBuilder::AddFilter ,将一个 filter添加到graph中。它不连接filter,并且在调用此方法前,filter必须已经被创建。创建filter可以是用 CoCreateInstance方法或使用Filter Mapper或系统设备枚举器(System Device Enumerator)。 
        这些方法提供了三种构建graph的途径: 
        1.filter graph manager构建整个graph 
        2.filter graph manager构建部分graph 
        3.应用程序构建整个graph 
    Filter Graph Manager构建整个graph 
        如果你仅仅是想回放一个已知格式的文件,如AVI、MPEG、WAV或MP3,使用RenderFile方法。 
        RenderFile方法首先寻找注册在系统中能分析源文件的filter,它使用协议名(如http://),文件扩展名或文件的头几个字节来决定选择哪一个源filter。 
        Filter Graph Manager使用一个迭代过程来完成余下的graph构建。在这个迭代过程中,它逐个列出filter的输出pin上支持的媒体类型,并搜索哪个已注册的filter的输入Pin接受该媒体类型。它使用一系列的规则来缩小filter的范围并排定优先顺序: 
        *filter类别 (category)标识的filter的一般功能 
        *媒体类型描述filter能在接受或能输出哪种数据类型 
        *merit 值决定filter被尝试的次序。如果两个filter具有相同的filter类别并且同时支持相同的输入类型,Filter Graph Manager选择merit值大的那一个。一些filter故意给出一个小merit值是因为它是为特殊用途设计的,仅能由应用程序来将其添加到 graph。 
        Filter Graph Manager使用Filter Mapper对象来搜索已注册的filter。 
        每个filter被添加时,filter graph manager试着将其与前一个filter的输出pin连接。它们协商决定他们是否能连接,如果能,哪一种媒体类型被用来连接。如果新filter不能连接,filter graph manager丢弃它并尝试别一个,这个过程一直继续到每个流都被render为止。 
    Filter Graph Manager构建部分graph 
        如果不仅仅是播放一个文件,那么你的应用程序就必须做一些graph的构建工作。比如,一个视频采集应用程序必须先选择一个source filter并将其添加到graph中去。如果你需要将数据写入到一个AVI文件中,你必须添加一个AVI Mux和File Write filter。不过,也经常有可能让filter graph manager来完成整个graph,比如,你可以通过Render方法来render一个pin进行预览。 
    应用程序构建整个graph 
        在某些场合,你的应用程序需要添加和连接每个filter来构建graph。在这种情况下,你很可能明确地知道哪些filter需要加到graph中去。使用这种方式,应用程序通过调用 AddFilter方法添加每个filter,然后枚举filter上的pin,调用Connect或ConnectDirect来连接它们。

    3.3.3. 智能连接 
        智能连接是filter graph manager用以构建filter graph的机制。它包含了一系列相关的用以选择filter和将它们添加到graph中去的算法。作为应用程序开发者,你并不需要很具体地了解智能连接的细节。如果你在构建某个filter graph时遇到问题并希望能解决它,或者你正在编写你自己的filter并希望它能自动地被graph构建,请阅读这一节。 
        智能连接涉及以下IGraphBuilder方法: 
        *IGraphBuilder::Render 
        *IGraphBuilder::AddSourceFilter 
        *IGraphBuilder::RenderFile 
        *IGraphBuilder::Connect 
        Render 方法构建一部分graph,它从一个尚未连接的输出pin开始顺着数据流的方向往下,添加必要的filter,起始的那个filter必须已被添加到了 graph中。Render方法每一步都搜索一个能够连接到前一个filter的filter,如果新连接上的filter有多个输出pin,数据流能自动分流,搜索直到每个流都被renderer为止。如果Render方法搜索到的filter无法使用,它会返回去尝试另一个filter。 
        要连接每一个输出pin,Render方法做以下工作: 
        1.如果pin支持IStreamBuilder接口,Filter Graph Manager让pin的IStreamBuilder::Render方法来完成整过程。通过暴露这个接口,pin承担了构建graph剩余部分的全部工作。但是,只有很少数的filter支持此接口。 
        2.Filter Graph Manager尝试使用任何在缓存中的filter。在智能连接的整个过程中,filter graph manager可以在早期将filter缓存起来。 
        3.如果filter graph包含了任何有未连接的输入pin的filter,filter graph manager会将其当作下一个filter来尝试连接。你可以通过在调用Render之前添加特定的filter来强制让Render方法来尝试这个 filter。 
        4.最后,filter graph manager使用IFilterMapper2::EnumMatchingFilters方法在所有注册的filter中寻找,依据已注册的媒体类型列表来逐个试着匹配输出pin的各个媒体类型(按优先级高低排列)。 
        每个已注册的filter都有一个merit值,这是一个用来表示 filter优先级的数字,最大优先级越高,EnumMatchingFilters方法返回的filter集依据merit值来排列,直至最小的 merit值MERIT_DO_NOT_USE+1,它忽略merit为MERIT_DO_NOT_USR或更小的filter。filter也通过 GUID来归类,类别本身也有merit值,EnumMatchingFilters方法忽略任何merit值为MERIT_DO_NOT_USE或更小的类别,即使在那个类别中的filter有较高的merit值。 
        总结一下,Render方法以下列步骤尝试filter 
        1. 使用IStreamBuilder 
        2.尝试被缓存的filter 
        3.尝试已添加在graph中的filter 
        4. 在已注册的filter中寻找 
        AddSourceFilter方法添加一个能render特定文件的source filter。首先,它依据协议名(如Http://)、文件扩展名、或文件头在已注册的filter中寻找匹配的那个。如果此方法定位到了一个合适的 source filter,它便立刻创建一个这个filter的实例,并将其添加到graph中,然后调用filter的 IFileSourceFilter::Load方法。 
        RenderFile方法依据一个文件名来构建一个默认的回放graph,在其内部,RenderFile方法调用AddSourceFilter来定位source filter,并且用Render来构建Graph的余下部分。 
        Connect 方法将输出pin连接到输入pin上去,这个方法自动添加必要的中间filter到graph中去,使用在Render方法中描述的那一系列算法: 
        1. 使用IStreamBuilder 
        2.尝试被缓存的filter 
        3.尝试已添加在graph中的filter 
        4. 在已注册的filter中寻找


    --  作者:admin 
    --  发布时间:2005-11-26 2:51:00 
    -- 

    3.4. Filter Graph中的数据流 
        这一节主要描述媒体数据是如何在filter graph中流动的。如果你只是为了编写DirectShow应用程序,你不需要知道这些细节,当然,知道这些细节对于编写directshow应用程序仍然是有帮助的。但是如果你要编写directshow filter,那么你就必须掌握这部分知识了。 
    3.4.1. DirectShow数据流概述 
        在这一部分先粗略地描述一下DirectShow中数据流是如何工作的。 
        数据首先是被保存在缓冲区里的,在缓冲区里,它们仅仅是一个字节数组。每一个缓冲区被一个称作媒体样本(media sample) 的COM对象所包容,media sample提供IMediaSample接口。media sample由另一个称作分配器(allocator) 的COM对象创建,allocator提供IMemAllocator接口。每一个pin连接都指定有一个allocator,当然,两个或多个pin连接也可以共享几个allocator。

        每一个allocator都创建一个media sample池,并为每个sample分配缓冲区。一旦一个filter需要一个缓冲区来填充数据,它就调用IMemAllocator::GetBuffer方法来请求一个sample。只要allocator有一个sample还没有被任何filter使用,GetBuffer方法就立即返回一个sample 的指针。如果allocator所有的sample已经被用完,这个方法就阻塞在那里,直到有一个sample变成可用的了。GetBuffer返回一个 sample后,filter就将数据写入到sample的缓冲区中去,并在sample上设置适当的标记(如时间戳),然后将它递交到下一个 filter去。 
        当一个renderer filter接收到了一个sample时,renderer filter检查时间戳,并将sample先保存起来,直到filter graph的参考时钟指示这个sample的数据可以被render了。当filter将数据render后,它就将sample释放掉,此时 sample并不立即回到allocator的sample池中去,除非这个sample上的参考计数已经变为0,表示所有的filter都已释放这个 sample。

        上游的filter可能在renderer之前运行,这就意味着,上游的filter填充缓冲的速度可能快于 renderer销毁它们。但是尽管如此,samples也并无必要更早地被render,因为renderer将一直保存它们直到适当的时机去 render,并且,上游filter也不会意外地将这些samples的缓冲覆盖掉,因为GetSample方法只会返回那些没有被使用的 sample。上游filter可以提前使用的sample的数量取决于allocator分配池中的sample的数量。 
        前面的图表只显示了一个allocator,但是通常的情况下,每个流中都会有多个allocator。因此,当renderer释放了一个sample时,它会产生一个级联效应。如下图所示,一个decoder保存了一个视频压缩帧,它正在等待renderer释放一个sample,而parser filter也正在decoder去释放一个sample。

        当renderer释放了一个sample后,decoder完成尚未完成的GetBuffer调用。然后decoder便可以对压缩的视频帧进行解码并释放它保存的sample,从而使parser完成它的GetBuffer调用。 
    3.4.2. 传输协议(Transports) 
        为了使媒体数据能在filter graph中流动,Directshow filter必须能支持多个协议中的一个,这些协议被称作传输协议(transports) 。当两个filter连接后,它们必须支持同一个传输协议,否则,它们将不能交换数据。通常,一个传输协议要求某个pin支持一个特定的接口,当两个 filter连接时,另一个pin来调用这个pin的这个接口。 
        大多数的directshow filter在主存中保存媒体数据,并且通过pin连接向另一个filter递交数据,这种类型的传输协议被称作本地内存传输协议(local memory transport) 。尽管这类传输协议在directshow中应用最普遍,但并非所有的filter都使用它。例如,某些filter通过硬件途径来传递数据,使用pin仅仅是为了传递控制信息,如IOverlay接口。 
        DirectShow为本地内存传输协议定义了两种机制,推(push)模式 和拉 (pull)模式 。在推模式中,source filter产生数据,并将其递交给下游的filter,下游的filter被动地接收数据并处理它们,再将数据传递给它的下游filter。在拉模式中,source filter与一个parser filter连接,parser filter向source filter请求数据,source filter回应请求并传递数据。推模式使用IMemInputPin接口,而拉模式使用IAsyncReader接口。 
        推模式比拉模式应用更广泛。 
    3.4.3. 媒体样本(sample)和分配器(allocator) 
        当一个pin向另一个pin传递媒体数据时,它并不是直接传递一个内存缓冲区的指针,而是传递一个COM对象的指针,这个COM对象管理着内存缓冲,被称为媒体样本(media sample) ,暴露IMediaSample接口。接收方pin通过调用IMediaSample接口的方法来访问内存缓冲,如IMediaSample::GetPointer,IMediaSample::GetSize和IMediaSample::GetActualDataLength。 
        sample 总是从输出pin到输入pin向下传输。在推模式中,输出pin通过在输入pin上调用IMemInputPin::Receive方法来传递一个 sample。输入pin或者在Receive方法内部同步地处理数据,或者另开一个工作线程进行异步处理。如果输入pin需要等待资源,允许在 Receive中阻塞。 
        另一个用来管理媒体样本的COM对象,被称作分配器(allocator) ,它暴露IMemAllocator接口。一旦一个filter需要一个空闲的媒体样本,它调用IMemAllocator::GetBuffer方法来获得sample的指针。每一个pin连接都共享一个allocator,当两个pin连接时,它们协商决定哪个filter来提供 allocator。pin可以设置allocator的属性,比如缓冲的数量和每个缓冲的大小。 
        下图显示了allocator、 media sample和filter的关系:

    媒体样本参考计数(Media Sample Reference Counts) 
        一个allocator创建的是一个拥有有限个sample的sample池。在某一时刻,有些sample正在被使用,有些则可被GetBuffer方法使用。allocator使用参考计数来跟踪sample,GetBuffer方法返回的sample参考计数为1,如果参考计数变为0,sample就可以返回到allocator的sample池中去了,这样它就可以再次被GetBuffer方法使用。在参考计数大于0期间,sample是不能被 GetBuffer使用的。如果每个从属于allocator的sample都在被使用,则GetBuffer方法会被阻塞直至有sample可以被使用。 
        举个例子,假设一个输入pin接收到一个sample。如果它同步地在Receive方法内部处理它,sample的参考计数不增加,当Receive返回时,输出pin释放这个sample,参考计数归0,sample就返回到sample池中去了。另一种情况,如果输入pin异步地处理sample,它就在Receive方法返回前将sample的参考计数加1,此时参考计数变为2。当输出pin释放这个sample时,参考计数变为1,sample不能返回到sample池中去,直到异步处理的工作线程完成工作,调用Release释放这个sample,参考计数变为0时,它才可以返回到sample池中去。 
        当一个pin接收到一个sample,它可以将数据拷贝到另一个sample中去,或者修改原始的 sample并将其传递到下一个filter中去。一个sample可能在整个graph长度内被传递,每个filter都依次调用AddRef和 Release。因而,输出pin在调用Receive后一定不能重复使用同一个sample,因为下游的filter可能正在使用这个sample。输出pin只能调用GetBuffer来获得新的sample。 
        这个机制减少了总的内存分配过程,因为filter可以重复使用同样的缓冲。它同样防止了数据在被处理前意外地被覆盖写入。 
        当filter处理数据后数据量会变大(如解码数据),一个filter可以为输入 pin和输出pin分配不同的allocator。如果输出数据并不比输入数据量要大,filter可以用替换的方式来处理数据而不用将其拷贝到新的 sample中去,在这种情况下,两个或多个pin连接共享一个allocator。 
    提交 (Commit)和反提交(Decommit)分配器 
        当一个filter首次创建一个allocator 时,allocator并不为其分配内存缓冲,此时如果调用GetBuffer方法的话会失败。当流开始流动时,输出pin调用IMemAllocator::Commit来提交allocator,从而为其分配内存。此时pin可以调用GetBuffer了。 
        当流停止时,pin调用IMemAllocator::Decommit来反提交allocator,在allocator被再次提交前所有后来的GetBuffer调用都将失败,同样,如果有阻塞的正在等待sample的 GetBuffer调用,也将立即返回失败信息。Decommit方法是否释放内存取决于实现方式,如CMemAllocator类直至析构时才释放内存。 
    3.4.4. filter状态 
        filter有三种可能的状态:停止(stopped) ,就绪(paused) 和运行(running) 。就绪状态的目的是为了让graph提前做准备以便在run命令下达时可以立即响应。Filter Graph Manager控制所有的状态转换。当一个应用程序调用IMediaControl::Run,IMediaControl::Pause或IMediaControl::Stop时,Filter Graph Manager在所有filter上调用相应的IMediaFilter方法。在停止状态和运行状态之间转换时总是要经过就绪状态,即如果应用程序在一个处于停止状态的graph上调用Run时,Filter Graph Manager在运行它之前先将其转为pause状态。 
        对于大多数 filter来说,运行状态和就绪状态是等同的。看下面的这个graph: 
        Source > Transform > Renderer 
        假设这个source filter不是一个实时采集源,当source filter就绪时,它创建一个线程来尽可能快地产生新数据并写入到media sample中去。线程通过在transform filter的输入pin上调用IMemInputPin方法将sample“推”到下游filter。transform filter在source filter的线程中接收数据,它可能也使用一个工作线程赤将sample传递给renderer,但是在通常情况下,它在同一个线程中传递它们。如 renderer处理就绪状态下,它等待接收sample,当它接收到一个时,它或阻塞或保存那个sample,如果这是一个Video renderer,则它将sample显示为一个静态的图片,只在必要的时候刷新它。 
        此时,流已经准备充分去被render,如果 graph仍然处理就绪状态下,sample会在每一个sample后堆积,直至每个filter都被阻塞在Receive或GetBuffer下。没有数据会被丢失。一旦source线程的阻塞被解除时,它只是简单地从阻塞点那里进行恢复。 
        source filter和transform filter忽略从就绪状态转到运行状态——它们仅仅是尽可能快地继续处理数据。但是当renderer运行时,它就要开始render sample了。首先,它render在就绪状态下保存的那个sample,接着,每接收到一个新的sample,它计算这个sample的呈现时间,renderer保存每个sample直至到了它们的呈现时间再render它们。在等待合适的呈现时间时,它或者阻塞在Receive方法上,或者在一个工作线程中接收数据并将其放入队列中去。renderer的上一个filter不关心这些问题。 
        实时源(live source),如采集设备,是通常情况中的一个例外。在实时源中,不适合提前准备数据。应用程序可能将graph置于就绪状态下,然后等很长时间才再运行它。graph不应该再render就绪期间的sample,因此,一个实时源在就绪状态时不产生新的sample。要将这种情况通知给filter graph manager,source filter的IMediaFilter::GetState方法返回VFW_S_CANT_CUE。这个返回值表示filter已切换到就绪状态下,即使renderer还没有收到任何数据。 
        当一个filter停止时,它不再接收任何传递给它的数据。source filter关闭它们的流线程,别的filter关闭所有它们创建的工作线程。pin反提交(decommit)它们的allocator。 
    状态转换 
        filter graph manager按从下游filter到上游filter的次序来完成所有的状态转换,从renderer开始逐个向上直至source filter,这个次序是必要的,可以防止数据丢失或graph死锁。最重要状态转换是就绪状态和停止状态间的转换: 
        *停止状态到就绪状态:当每一个filter被置为就绪态时,它便准备好从上一个filter接收sample。source filter是最后一个被置为就绪态的filter,它创建数据流线程并开始传递sample。因为所有下游filter都处于就绪状态,所以没有一个 filter会拒绝接收sample。当graph中所有的renderer都接收到一个sample后,filter graph manager才彻底完成状态转换工作(实时源除外)。 
        *就绪状态到停止状态:当一个filter停止时,它释放了所有它保存的 sample,就将解除所有上游filter调用GetBuffer时的阻塞。如果filter正在Receive方法中等待数据,则它停止等待并从 Receive中返回,从而解除阻塞。因而,此时当filter graph manager再去将上游filter转换为停止状态时,它已经不再阻塞于GetBuffer和Receive,从而可以响应停止命令。上游filter 在得到停止命令前可能会传递下一些过时的sample,但下游filter不再接收它们,因为此时下游filter已处于停止状态了。 
    3.4.5. 拉模式 
        在IMemInputPin接口中,上游filter决定哪些数据要被发送,然后将数据推到下游filter 中去。但是在某些情况下,拉模式会更加合适。在拉模式中,只有当下游filter从上游filter中请求数据时,数据才被传递下去,数据流动由下游 filter发起。这种类型的连接使用IAsyncReader接口。 
        典型的拉模式应用是文件回放。比如,在一个AVI回放graph中,Async File Source filter完成一般的文件读操作并将数据作为字节流传递下去,没有什么格式信息。AVI Splitter filter读取AVI头并将数据流分解成视频和音频sample。AVI Splitter比Async File Source filter更能决定它们需要哪些数据,因此需用IAsyncReader接口来代替IMemInputPin接口。 
        要从输出pin请求数据,输入pin调用下面方法中的一个: 
        *IAsyncReader::Request 
        *IAsyncReader::SyncRead 
        *IAsyncReader::SyncReadAligned 
        第一个方法是异步的,支持多重读操作。其余的是同步的。 
        理论上,任一个filter都能支持IAsyncReader,但是实际上,它仅仅在连接有一个parser filter的source filter上使用。分析器(parser)非常象一个推模式的source filter,当它就绪时,它创建一个数据流线程,从IAsyncReader连接中拉数据并将其推到下一游filter中去。它的输出pin使用 IMemInputPin,graph余下的部分使用标准的推模式。


    --  作者:admin 
    --  发布时间:2005-11-26 2:52:00 
    -- 

    3.5 DirectShow中的事件通告 
        这一节主要描述在 directshow filter graph中事件是怎样发生的,以及应用程序如何接收事件通告并响应它们。
    3.5.1 概述 
        一个filter通过发送一个事件通来通知filter graph manager某个事件已经发生。这些事件可以是一些预知的事件比如流结束事件,也可以是一些异常如render流时失败。一部分事件由filter graph manager自己处理,另一部分则由应用程序来处理。如果filter graph manager不处理某个事件,那么这个事件会被放入到队列中去。filter graph也可以通过队列将自己的事件发送给应用程序。 
        应用程序从队列中接收事件并根据其类型来响应它们。DirectShow中的事件通告类似于windows的消息队列机制。应用程序可以让filter graph manager取消对指定的事件类型的默认操作,而是将它们放入事件队列由应用程序来处理它们。 
        由于这样的机制,使我们能做到: 
         *filter graph manager与应用程序的对话 
         *filter可以即和应用程序也和filter graph manager对话 
         *由应用程序来决定处理事件的复杂度。

    3.5.2 从队列中取事件 
        Filter Graph Manager暴露3个支持事件通知的接口: 
         *IMediaEventSink 包含filter发送事件的方法 
         *IMediaEvent 包含应用程序取事件的方法 
         *IMediaEventEx 继承扩展IMediaEvent接口 
        filter 通过在filter graph manager上调用IMediaEventSink::Notify方法来发送事件通告,一个事件通知由一个表示事件类型的事件号,和两个DWORD类型用以放置附加信息的参数组成。按事件号的不同,这两个参数可以是指针、返回值、参考时间或者其它信息。完整的事件号和参数列表,参见Event Notification codes(http://msdn.microsoft.com/library/en-us/directshow/htm/eventnotificationcodes.asp )。 
        要从事件队列中取事件,应用程序需要在filter graph manager上调用IMediaEvent::GetEvent事件。这个方法一直阻塞到取到事件或超时。一旦队列中有了事件,这个方法就返回事件号和两个事件参数。在调用GetEvent后,应用程序应该总是调用IMediaEvent::FreeEventParams方法来释放与事件参数相关的所有资源。比如,一个参数可能是由filter graph分配的BSTR值。 
        下面的代码是一个如何从队列中取事件的框架:

      long evCode, param1, param2; 
      HRESULT hr; 
      while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr)) 
      { 
          tch(evCode) 
          { 
              // Call application-defined functions for each 
              // type of event that you want to handle. 
          } 
          hr = pEvent->FreeEventParams(evCode, param1, param2); 
      }

      要重置filter graph manager默认的事件处理过程,调用IMediaEvent::CancelDefaultHandling方法,用事件号做参数。你可以通过调用 IMediaEvent::RestoreDefaultHandling方法来恢复某个事件的处理过程。如果filter graph对某个事件号没有默认处理过程,则调用上面两个方法不产生任何影响。

    3.5.3 当事件发生时 
        要处理DirectShow事件,应用程序需要一个方法来知道事件何时正等待在队列中。Filter Graph Manager提供两种方法: 
        *窗口通告:一旦有事件发生,Filter Graph Manager就发送一个用户自定义窗口消息来通知应用程序窗口 
        *事件信号:如果有DirectShow 事件在队列中,filter graph manager就触发一个windows事件,如果队列为空,则reset这个事件。 
        应用程序可以使用任何一种方法,但通常窗口通告方法相对比较简单。 
    窗口通告: 
        要设置窗口通告,调用IMediaEventEx::SetNotifyWindow方法并指定一个私有消息,私有消息可以是从WM_APP到0xBFFF的任一个。一旦filter graph manager把一个新的事件通告放入队列中,它便发送这个消息给指定的窗口。应用程序从窗口的消息循环中来响应这个消息。 
        下面是如何设置通知窗口的例子:

       #define WM_GRAPHNOTIFY WM_APP + 1   // Private message. 
      pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);

      消息是一个普通的windows消息,并且独立于DirectShow消息通告队列被发送。使用这种方法的好处是大部分应用程序拥有一个消息循环,因此,要知道DirectShow事件何时发生便无需做额外的工作了。 
      下面是一段如何响应通告消息的框架代码:

       LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, UINT wParam, LONG lParam) 
      { 
          tch (msg) 
          { 
              case WM_GRAPHNOTIFY: 
                  HandleEvent();  // Application-defined function. 
                  break; 
              // Handle other Windows messages here too. 
          } 
          return (DefWindowProc(hwnd, msg, wParam, lParam)); 
      }

          因为事件通告与消息循环均为异步进行的,因此在应用程序响应事件时队列中可以会有多个事件。而当事件变为非法时,它们会从队列中被清除掉。所以在你的事件处理代码中,调用GetEvent直至返回一个表示队列已空的失败代号。 
        在释放 IMediaEventEx指针前,请以NULL作参数调用SetNotifyWindow方法来取消事件通告。并且在你的事件处理代码中,在调用 GetEvent前检查IMediaEventEx指针是否合法。这些步骤可以防止在释放IMediaEventEx指针后应用程序继续接收事件通告的错误。 
    事件信号: 
        Filter Graph Manager建立一个反映事件队列状态的手工重设事件(manual-reset event)。如果队列中包含有未处理的事件通告,Filter Graph Manager就会发信号给手工重设事件。如果队列是空的,则调用IMediaEvent::GetEvent方法会重设(reset)事件。应用程序可以通过这个事件来确定队列的状态。 
        注意:此处的术语可能被混淆。手工重设事件是由windows的 CreateEvent函数创建的一种事件类型,它与由DirectShow定义的事件无关。 
        调用 IMediaEvent::GetEventHandle方法得到手工重设事件的句柄,调用一个函数如WaitForMultipleObjects来等待发送给手工重设事件的信号。一旦收到信号,就可以调用IMediaEvent::GetEvent来接收DirectShow事件了。
        下面的代码举例说明了这种方法。在取得事件句柄后,在100毫秒时间间隔内等待发送给手工重设事件的信号,如果有信号发来,它调用GetEvent然后在 windows控制台上打印出事件号和事件参数,循环在EC_COMPLETE事件发生后结束,这标志着回放结束。

    HANDLE  hEvent; 
      long    evCode, param1, param2; 
      BOOLEAN bDone = FALSE; 
      HRESULT hr = S_OK; 
      hr = pEvent->GetEventHandle((OAEVENT*)&hEvent); 
      if (FAILED(hr) 
      { 
          /* Insert failure-handling code here. */ 
      } 
      while(!bDone) 
      { 
          if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent, 100)) 
          { 
              while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr)) 
              { 
                  printf("Event code: %#04x//n Params: %d, %d//n", evCode, param1, param2); 
                  pEvent->FreeEventParams(evCode, param1, param2); 
                  bDone = (EC_COMPLETE == evCode); 
              } 
          } 
      }

        因为Filter Graph会在适当的时候自动重设事件,因此你的应用程序应当不去作重设工作。同时,当你释放filter graph时,filter graph会关闭事件句柄,因此在这之后你就不能再使用事件句柄了。


    --  作者:admin 
    --  发布时间:2005-11-26 2:53:00 
    --   
    3.6. DirectShow中的时间和时钟 
        这一节主要概述DirectShow体系中时间和时钟。 
    3.6.1. 参考时钟 
        Filter Graph Manager的一个功能,能够以同一个时钟来同步所有在graph中的filter,称作参考时钟(reference clock)。 
        任何暴露了IReferenceClock接口的对象都能够作为一个参考时钟来使用。参考时钟可以由一个 DirectShow filter来提供,例如可以直接使用硬件时钟的audio renderer。另外,Filter Graph Manager也能使用系统时间来作参考时钟。 
        名义上,一个参考时钟以千万分之一秒的精度来度量时间,但是实际上的精度不会这么高。要取得参考时钟的当前时间,调用IReferenceClock::GetTime方法。由于时钟的基准时间,即时钟开始时的时间计数,是依赖于具体的实现的,因此GetTime的返回值不反映绝对时间,只反映相对于graph开始时的相对时间。 
        虽然参考时钟的精度是变化的,但是 GetTime的返回值却保证是单调递增的,换句话说,也就是参考时钟的时间是不会回退的。如果参考时钟的时间是由硬件源产生的,而硬件时钟回退了(比如,有一个调节器调节了时钟),GetTime依然返回最晚的那个时间只到硬件时钟追上它。要知道更多的内容可以参考 CBaseReferenceClock类(http://msdn.microsoft.com/library/en-us/directshow/htm/cbasereferenceclockclass.asp )。 
    默认参考时钟 
        当Graph运行时,Filter Graph Manager会自动选择参考时钟,选择参考时钟的规则如下: 
        *如果应用程序指定了一个时钟,则使用这个时钟; 
        *如果 Graph包含了一个支持IReferenceClock的活动源filter(即推模式源filter),则使用这个filter; 
        *如果Graph未包含任何支持IReferenceClock的推模式源filter,使用任何一个支持IReferenceClock接口的 filter,选择的次序是从Renderer filter开始依次向上。已连接的filter优先于未连接的filter被选。(如果这个graph会render一个音频流,则这个规则通常就会选择audio renderer filter来作为参考时钟) 
        *如果没有filter支持合适的时钟,则使用系统参考时钟。 
    设置参考时钟 
        应用程序可以在Filter Graph Manager上调用IMediaFilter::SetSyncSource方法来选择时钟,只有在由于你有一个特殊原因想要选择自己的时钟时才需要这么做。 
        想要让Filter Graph Manager不使用任何参考时钟,可以调用SetSyncSource,参数为NULL。比如,你需要尽可能快地来处理sample时,就可以这么做。要恢复黑认的参考时钟,在Filter Graph Manager上调用IFilterGraph::SetDefaultSyncSource方法。 
        当参考时钟发生变化时,Filter Graph Manager会通知每一个filter调用它的IMediaFilter::SetSyncSource方法,应用程序无需调用filter的这个方法。 
    3.6.2. 时钟时间 
        DirectShow定义了两种相关时间:参考时间(reference time)和流时间 (stream time) 
        *参考时间是一个绝对时间,由参考时钟返回 
        *流时间是一个相对于graph最后开始时的相对时间 
         ·当graph处于运行态时,流时间等于参考时间减去起始时间 
         ·当graph处于暂停态时,流时间停留在暂停的那一刻 
         ·在重新定位后,流时间被重设为0 
         ·当graph处于停止态时,流时间无意义 
        如果一个媒体样本有一个时间戳t,表示这个在流时间为t时被render,正因为这个原因,因此流时间也被叫做呈现时间(presentation time)。 
        当应用程序调用IMediaControl::Run运行graph时,Filter Graph Manager调用每个filter的IMediaFilter::Run。为了补偿消耗在运行每个filter的时间总和,Filter Graph Manager会略微晚一点来定义起始时间。 
    3.6.3. 时间戳 
        时间戳定义了媒体样本的起始和结束时间。时间戳有时被称作呈现时间(presentation time)。在阅读余下的文章时,一个必须记住的要点是并非所有的媒体格式都以相同的方式来使用时间戳。举个例子,并不是所有MPEG样本都被打上了时间戳,在MPEG Filter Graph中,时间戳在被解码前并非应用在每个帧上。 
        当一个renderer filter接收到一个样本时,它以时间戳为基准来确定render时间。如果样本来晚了,或者这个样本没有时间戳,那个filter就立刻render 它,否则,filter就等在那直到合适的时机。(通过IReferenceClock::AdviseTime方法来等待样本的render时间) 
        源 filter和语法解析filte使用下列原则,在它们处理的样本上设置合适的时间戳: 
        *文件回放:第一个样本被打上起始时间戳,为0,后面的时间戳由样本长度和回放速率来决定,这些都由文件格式来决定。分析文件的filter负责计算出合适的时间戳。例子见(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directshow/htm/avisplitterfilter.asp ) 
        *音视频采集:每个样本都被打上一个起始时间戳,这个时间戳与当它被捕获时的Stream time相同。应注意以下几点: 
         ·从预览pin(Preview Pin)出来的样本没有时间戳。因为图像处理的延时,一个打上采集时间的视频帧总是会迟一点到达视频renderer。这会导致在进行质量控制时,renderer会丢弃部分视频帧。关于质量控制,参见(http://msdn.microsoft.com/library/en-us/directshow/htm/qualitycontrolmanagement.asp ) 
         ·音频采集:音频采集filter使用它自己的缓冲集,而并非使用音频驱动程序的。音频驱动以固定的时间间隔来填充采集filter的缓冲。这个时间间隔由驱动决定,通常不超过10毫秒。在音频样本上的时间戳反映的是驱动填充采集filter时的时间,因此会有偏差,尤其是当应用程序使用一个很小的缓冲区时。不过,媒体时间可以精确地反映缓冲区中音频样本的数量。 
        *混合filter(Mux filter):依赖于输出格式,一个mux filter可能需要由它来产生时间戳,也可能不需要。举个例子,AVI文件格式使用固定的帧率而没有时间戳,因此AVI Mux filter假设那些样本在近似正确的时间内到达。如果样本没有在合适的时间间隔内到达,AVI Mux filter会插入一个长度为0的空样本,来表示一个丢失的帧。在文件回放时,新的时间戳在运行时如前面所述地那样产生。 
        要在一个样本上设置一个时间戳,调用IMediaSample::SetTime方法。 
        此外,filter还可以为样本指定一个媒体时间(media time)。在视频流中,media time表示视频帧的数量。在音频流中,media time表示包中的样本数量,比如,如果每个包包含以44.1KHz的采样率采集的一秒钟的音频,那么第一个包具有一个为0的媒体起始时间以及为 44100的媒体终止时间。在一个可以定位的流中,媒体时间总是相对于流的起始时间,比如,假设你在一个15帧/秒的视频流上定位到2秒这个位置,那么定位后的每一个媒体样本的时间媒为0,但是它的媒体时间为30. 
        Renderer和Mux filter能使用媒体时间通过检查是否有缺口来确定帧或样本是否被丢弃了。但是,filter不是一定要设定媒体时间。要设置媒体时间,调用 IMediaSample::SetMediaTime方法。 
    3.6.4 实时源(Live Source) 
        实时源,也被叫做推模式源(push source),实时地接收数据,比如视频采集和网络广播。通常情况下,一个实时源不能控制数据到达的速率。 
        一个filter被认为是实时源需要具有以下几点: 
        * 调用IAMFilterMiscFlags::GetMiscFlags方法时返回AM_FILTER_MISC_FLAGS_IS_SOURCE标记,并且至少有一个输出pin暴露IAMPushSource接口。 
        * filter暴露IKsPropertySet接口,并具有一个capture pin(PIN_CATEGORY_CAPTURE)。 
    延时(latency) 
        一个filter的延时是这个filter处理一个样本所需的时间总和。在实时源中,延时取决于保存样本的缓冲区大小。举个例子,假设graph有一个具有33ms延时的视频源和一个具有500ms延时的音频源,那么每个到达视频renderer的视频帧要比与之匹配的音频样本到达音频renderer早470ms,除非 graph对这个差别进行补偿,否则音视频将会不同步。 
        实时源可以通过IAMPushSource接口来进行同步。Filter Graph Manager并做同步工作除非应用程序通过调用IAMGraphStreams::SyncUsingStreamOffset方法来激活它。如果同步被激活,Filter Graph Manager通过IAMPushSource来查询每一个source filter,如果filter支持IAMPushSource,那么Filter Graph Manager调用IAMLatency::GetLatency来得到filter预期的延时(IAMPushSource继承自 IAMLatency)。通过组合的延时值,filter graph manager决定graph中最大的预期延时,然后调用IAMPushSource::SetStreamOffset来给每一个source filter一个流偏移,以后filter会在产生时间戳时加上这个偏移。 
        这个方法主要是为了实现实时预览,但是,注意实时采集设备(比如摄像头)的preview pin上是没有时间戳的,因此,要在一个实时采集设备上使用这种方法,你必须在capture pin上进行视频预览。 
        通常,IAMPushSource接口被VFW Capture filter和音频采集filter(Audio capture filter)支持。 
    速率匹配(Rate Matching) 
        如果 renderer filter和source filter使用不同的参考时钟,那么就会有问题,renderer可能比source要快,这就导致了数据的缺口,或则renderer比source 慢,就会导致数据拥堵而样本丢弃。通常一个实时源无法控制速率,因此要求renderer来与source进行速率匹配。 
        通常,只有 audio renderer实现速率匹配,因为声音回放的频率比视频更重要。要实现速率匹配,audio renderer必须排除以下几点: 
        *如果graph没有使用一个参考时钟,那么audio renderer不会去进行速率匹配(如果graph没有参考时钟,那么样本总是在到达时就被立刻render)。 
        *另外,如果 graph中有一个参考时钟,audio renderer检测是否有一个实时源在上游,如果没有,audio renderer不进行速率匹配。 
        *如果有一个实时源在上游,并且这个实时源在它的输出Pin上暴露IAMPushSource接口,audio renderer调用IAMPushSource::GetPushSourceFlags,并寻找以下标记: 
         ·AM_PUSHSOURCECAPS_INTERNAL_RM,这个标记表示这个实时源拥有自己的速率匹配机制,因此audio renderer不进行速率匹配。 
         ·AM_PUSHSOURCECAPS_NOT_LIVE,这个标记表示source filter并不是一个真正的实时源,即使它暴露了IAMPushSource接口,因此,audio renderer不进行速率匹配。 
         ·AM_PUSHSOURCECAPS_PRIVATE_CLOCK,这个标记表示source filter使用一个私有的时钟来产生时间戳。在这种情况下,audio renderer速率匹配与时间戳会有冲突。(如果样本没有时间戳,那么renderer忽略这个标记。 
        *如果 GetPushSourceFlags返回没有标记(0),audio renderer的行为依赖于graph时钟和样本是否拥有时间戳: 
         ·如果audio renderer不是graph参考时钟,并且样本拥有时间戳,那么audio renderer速率匹配与时间戳会有冲突 
         ·如果样本没有时间戳,audio renderer尝试与输入的音频数据的速率进行匹配。 
         ·如果audio renderer是graph参考时钟,它与输入的数据速率进行匹配。 
        最后一种情况的原因如下:如果audio renderer是参考时钟,并且source filter使用同样的时钟来产生时间戳,那么audio renderer不会与这个时间戳进行速率匹配,因为如果它这样做了,导致的结果是,它等于在尝试与自己进行速率匹配,这将导致时钟偏差。因此,在这种情况下,renderer与输入的音频数据速率进行匹配。


    --  作者:admin 
    --  发布时间:2005-11-26 2:53:00 
    -- 

    3.7. Graph动态重建(Dynamic Graph Building) 
        如果你需要修改一个已经存在的filter graph,你可以停止,修改后再重新启动它。这通常是一种最佳的解决方法。但是,在某此情况下,你可能需要在一个graph处于运行状态时来修改它,比如: 
        *应用程序在进行视频回放时需要插入一个(视频滤镜filter)Video effect filter; 
        *source filter在播放的过程中改变了媒体格式,此时可能需要接入新的解码filter; 
        *应用程序在graph中加入一个新的视频流。 
        上面的这些都是graph动态重建的例子。所有在graph继续处于运行状态而做的graph修改都被叫做graph动态重建。动态重建可以由应用程序发起,也可以由一个在graph中的filter发起。动态重建有三种可能: 
        *媒体格式动态变化:一个filter可以在运行的中途改变媒体格式,而不需要重新被替换为另一个; 
        *动态重连:在graph中添加或删除filter 
        *Filter Chain操作:添加,删除,控制filter chain,(Filter Chain是相互连接着的一条Filter链路,并且链路中的每个Filter至多有一个Input pin,至多有一个Output pin) 
    3.7.1. 动态重连 
        在绝大多数的directshow filter中,当graph处于运行状态时pin是不能被重新连接的,应用程序必须在重连前停止graph。但是,某些filter却支持动态重连,这既可以由应用程序来执行,也可以由graph中的一个filter来执行。 
        如下图: 
       

        假设我们要将filter 2从graph中移除掉,替换成另一个filter,而此时graph还处于运行状态,那么必须具备以下几个条件: 
        *filter 3的输入pin(pin D)必须支持IPinConnection接口,这个接口可以重新连接pin而不需要停止它。 
        *filter 1的输出pin(pin A)必须能够在重连时阻塞媒体数据,数据不再在pin A和pin D之间传递。也就是说,输出Pin必须支持IPinFlowControl接口。但是,如果filter 1是发起重连的那个filter,那么它有可能已经在其内部实现了阻塞; 
        动态重连包括下列步骤: 
        1. 从Pin A那里阻塞数据流 
        2. 重新连接Pin A和Pin D,或者在中间加入新的filter 
        3. 取消Pin A上的阻塞

    步骤1. 阻塞数据流     通过调用Pin A上的IPinFlowControl::Block方法来阻塞数据流。这个方法既可以被同步调用,也可以被异步调用。要异步调用这个方法,需要创建一个win32事件对象,并将事件句柄传给Block,方法会立即返回,然后使用 WaitForSingleObject或其它函数来等待事件的触发。当阻塞工作完成时,pin会触发这个事件。如:

    // Create an event 
    HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 
    if (hEvent != NULL) 

        // Block the data flow. 
        hr = pFlowControl->Block(AM_PIN_FLOW_CONTROL_BLOCK, hEvent); 
        if (SUCCEEDED(hr)) 
        { 
            // Wait for the pin to finish. 
            DWORD dwRes = WaitForSingleObject(hEvent, dwMilliseconds); 
        } 
    }

        如果是同步调用Block,那么只需将传入的hEvent参数设为NULL,此时这个方法会一直阻塞到阻塞工作完成为止。如果pin还没有准备好deliver一个新的sample,那么就会一直阻塞。而如果filter处于就绪状态,这可能会花费任意长的时间,因此,不要在应用程序的主线程中使用同步调用,以免发生死锁,开一个工作线程来使用同步调用,或者干脆就使用异步调用。

    步骤2. 重连pin 
        要重新连接pin,查询graph的IGraphConfig接口并调用IGraphConfig::Reconnect或IGraphConfig::Reconfigure。 Reconnect方法使用比较简单: 
        *停止中间filter(比如filter 2),并移除它 
        *如果需要的话,加入新的中间filter 
        *连接所有的pin 
        *pause或run所有新的filter,使它的状态与graph相同 
        Reconnect 方法有参数可以用来指定pin连接的媒体类型和中间filter。如:

    pGraph->AddFilter(pNewFilter, L"New Filter for the Graph"); 
    pConfig->Reconnect( 
        pPinA,      // Reconnect this output pin... 
        pPinD,      // ... to this input pin. 
        pMediaType, // Use this media type. 
        pNewFilter, // Connect them through this filter. 
        NULL, 
        0);

        如果Reconnect还不够用来应付我们的要求,那么你可以使用Reconfigure方法,它调用一个由应用程序定义的回调函数来重连这些 pin。要调用这个方法,需要在你的应用程序中实现IGraphConfigCallback接口。 
        在调用Reconfigure之前,如前面所述地那样阻塞输出pin的数据流。然后如下所示,将处于待处理状态的数据push下去: 
        1. 在重连链路中处于下游的最远的那个输入pin(例子中为Pin D)上调用IPinConnection::NotifyEndOfStream方法,方法的参数是一个Win32事件句柄; 
        2. 在与要阻塞数据的那个输出pin直接相连的那个输入pin上调用IPin::EndOfStream方法。(在例子中,要阻塞的那个输出pin是pin A,那么直接与之相连的那个输入pin为Pin B); 
        3. 等待事件触发。输入pin(pin D)在它接收到end-of-stream事件通告时触发事件。这表示再没有数据需要传输,此时就可以安全地进行重连了。 
        注意:IGraphConfig::Reconnect方法会自动处理上述步骤,你仅在调用Reconfigure方法时才需要自己来处理。 
        当数据完成push后,调用Reconfigure,传入IGraphConfigCallback回调函数的指针。Filter Graph Manager会调用IGraphConfigCallback::Reconfigure方法。

    步骤3. 取消数据流的阻塞 
        当你完成重连后,通过调用IPinFlowControl::Block,第一个参数为0来取消阻塞。

        注意:如果动态重连是由一个filter来执行的,那么你需要知道一点线程方面的问题。如果filter graph manager尝试去停止filter,它可能会死锁,因为graph等待filter停止,而与此同时,filter有可能在等待数据在graph中完成push。要防止这个可能存在的死锁问题,如前所述可以用事件机制来处理。

    3.7.2. filter链(filter chains) 
        一个 filter chain是一系列具备下述条件的相互连接的filter: 
        *每一个在链中的filter最多只有一个已连接的输入pin 和一个已连接的输出pin; 
        *Filter链路中的数据流不依赖于链路外的其他Filter 
        举个例子,在下图中,filter A-B,C-D和F-G-H是一个filter chains。每个F-G-H中的子链(F-G和G-H)也是一个filter chain。一个filter chain同样可以是由单个filter组成的,因此A、B、C、D、F、G和H同样也是filter chain。filter E由于有两个输入连接,所以任何含有E的一系列filter都不是filter chain。 

        IFilterChain接口提供下述方法来控制filter chain:

    IFilterChain::StartChain  开启一个链 
    IFilterChain::StopChain   停止一个链 
    IFilterChain::PauseChain  暂停一个链 
    IFilterChain::RemoveChain  从graph中移除一个链

        没有特殊的方法来添加一个链,要添加链,通过调用IFilterGraph::AddFilter方法来插入新的filter,然后调用IGraphBuilder::Connect,IGraphBuilder::Render或类似的方法来连接它们。 
        当graph运行时,一个filter chain可以在运行和停止状态间切换。当graph处理就绪状态时,它可以在就绪和停止状态间切换。这是两种仅有的filter chain状态切换可能。 
    Filter链指南 
        当你使用IFilterChain方法时,确认在graph中的filter是否能支持filter链操作是十分必要的,否则,可能会发生死锁或graph 错误。filter连接到链上必须发生在链状态改变后。 
        使用IFilterChain的最佳情况是与一系统为链而设计的filter一起使用。使用下面的指南来确保你的filter是链操作安全的。参考下图:

           在 filter链状态变化前,所在在filter链分界线上调用的数据处理都必须已完成。这个规则应用于IMemInputPin::Receive、IPin::NewSeqment和IPin::EndOfStream方法。链中的filter必须从由链外filter实现的这些方法调用中返回;而链外的filter也必须从这些由链内filter实现的这些方法调用中返回。 
        举个例子,在上图中,filter B必须完成在filter A上的所有数据处理调用,而filter E也必须完成从filter D上的调用。如果pin暴露了IPinFlowControl和IPinConnection接口,那么如在动态重连那一节中所讲的,你可以通过调用IPinFlowControl::Block和IGraphConfig::PushThroughData方法来推数据。filter也可能通过自己的方法来推数据。 
     上游filter必须与链的状态一起发生变化。比如,在上图中,假如链已停止,但filter A调用IMemInputPin::Receive方法,那么调用将失败,作为回应,filter A停止流。当应用程序重新开启链时,不会产生什么影响,因为filter A不再向使数据流动了。 
     下游filter必须同样与链的状态一起发生变化,否则,下游filter在等待取得sample时会发生死锁,因为sample不会再到来了。比如,多路复用(MUX)filter总是在它所有的input pin上需要数据,如果挂起其中的一个input pin,在其它input pin上的流处理也会被阻塞。这会导致graph死锁 
     每个与链内部filter相连的外部 filter的pin必须拥有自己的分配器(allocator),它不能被其它pin连接共享。当链的状态发生变化或从graph移除掉时,分配器便不可用了,此时如果还有其它的连接使用这个分配器的话,它们将不能再处理sample了。 
     除非与链相连的filter支持动态断开,否则不要移除链。典型的,已连接的filter会支持IPinConnection或IPinFlowControl接口,或者用它自己定义的接口代替。


    --  作者:admin 
    --  发布时间:2005-11-26 2:54:00 
    -- 

    3.8. 插件发布者(Plug-in Distributors)

    Plug-in Distributors(PIDs)是扩展filter graph manager的一种方法。一个PID是filter graph manager在运行时聚合的一个COM对象。应用程序通过filter graph manager来进入PID。 
        当filter graph manager被要求查询一个它不支持的接口时,它会搜索注册表项: 
    HKEY_CLASSES_ROOT//Interface//IID//Distributor 
       IID是接口的GUID,如果注册项存在,那么键值便是支持该接口的PID类标识(CLSID)。filter graph manager聚合了PID并返回接口指针,应用程序调用这个指针时实际上就是在调用PID,但是这对于应用程序来说是透明的,对于应用程序来说,它就象是在filter graph manager上调用这个接口一样。 
       PID为应用程序提供了一种简单的控制filter的方法,如通过调用IFilterGraph::EnumFilters方法,PID可以枚举graph中的所有 filter并调用这些filter上的方法。 
       当filter graph manager聚合了一个PID时,它查询PID的IDistributorNotify接口,如果PID支持这个接口,filter graph manager用它来通知PID有关graph的状态变化: 
    * 当filter graph在run、pause和stop状态之间切换时,它调用IDistributorNotify::Run, IDistributorNotify::Pause或IDistributorNotify::Stop。 
    * 如果调置了参考时钟,filter graph manager调用IDistributorNotify::SetSyncSource。 
    * 当有filter添加或移除,或pin连接有变化时,filter graph manager调用IDistributorNotify::NotifyGraphChange。 
       当自己定制PID时,自己所创建的COM对象必须支持聚合,并且它所支持的接口是filter graph manager本身所没有的。IDistributorNotify接口是可选的。 
       如果PID从filter graph manager上获得一个接口,那它必须立即release这个接口,否则会在COM对象上出现循环引用的参考计数,使得filter graph manager无法被销毁。在filter graph manager上保持一个参考计数是多余的,因为PID的生命期是由filter graph manager控制的。 
       因为PID是明确指定是被filter graph manager聚合使用的,因此你应该在PID的构造函数中强行检查IUnknown指针是否为NULL,如果为NULL,则返回错误码 VFW_E_NEED_OWNER。同时,为了防止其它对象聚合PID,你可以在IUnknown上查询IGraphBuilder接口,如果不行则返回错误。


    --  作者:admin 
    --  发布时间:2005-11-26 2:55:00 
    --  奇妙”的Merit(玩死Media Player)

    运行GraphEdit,插入Filter,我们可以看到:每个Filter的信息一般包括Displayname、Filename、Merit、各个Pin以及Pin支持的Mediatype,还有Version等。我们今天就来看一看这个Merit(其他的一些Filter信息大家从它的名字上就可以猜到它的意义)。 
    要说Merit,肯定要先说Filter Graph Manager使用的智能连接(Intelligent Connect)机制。我们在Filter Graph中Render一个Pin,或者Render一个File,然后看到一条自动的“解码”Filter链路就完成了——这就是智能连接机制。执行这个机制的调用方法为:IGraphBuilder::RenderFile, IGraphBuilder::Render, 和 IGraphBuilder::Connect。下面分别对这三个调用方法进行阐述。 
    RenderFile :给出一个文件名,首先要找到正确的Source Filter。Filter Graph Manager通过查找注册表来决定使用什么Source Filter。在注册表中,一般会有文件扩展名或者特征字节与使用的Source Filter的对应信息。找到Source Filter之后,就从该Source Filter的各个Output pin开始,进行剩下的职能连接过程。这是一个“递归”过程,直到所有的分支都连到一个Renderer Filter上。步骤大致为: 
    1. 如果Output pin支持IStreamBuilder接口,则把剩下的工作交给IStreamBuilder::Render。 
    2. 使用在内存中缓冲的Filter进行是试连接。 
    3. 使用在当前Filter Graph中还没有完全连接的Filter进行试连接。(如果你想智能连接使用特定的你想使用的Filter,一种方法是,在开始智能连接之前先把该 Filter加入到Filter Graph中。) 
    4. 使用IFilterMapper2::EnumMatchingFilters搜索注册表。Filter Graph Manager使用Merit值大于MERIT_DO_NOT_USE的所有Filter(Filter所在的目录Merit值也应该大于 MERIT_DO_NOT_USE)进行试连接。在匹配Mediatype的前提下,Merit值越高,该Filter被使用的概率越高。 
    Render :这个方法从当前Filter Graph的某个Filter的指定Output pin开始,进行从这个Pin往下的一条支路的智能连接。智能连接的算法与上述RenderFile的类似。 
    Connect :这个方法调用,以欲连接的一对Input pin和Output pin作为参数。首先进行这两个Pin之间的直接连接。如果不能成功连接,则要插入“中介”Filter。这个“中介”Filter的插入过程就是一个智能连接过程,算法与上述的RenderFile类似。

    现在我们知道了智能连接是怎么回事。DirectShow的这个机制,很“聪明”,可以方便地使用第三方(非Microsoft公司)开发的Filter。但是,有一个问题,就是如果系统中存在一些“恶意”的Filter,那么这个智能连接机制就会受到严峻的考验。因为这个原因,基于智能连接机制的应用程序(比如Windows Media Player)也会变得不稳定。(笔者并不赞成直接使用DirectShow Editing Services API进行非线性编辑,就是出于这方面的考虑。)
    大家可以下载我写的这个测试Filter源代码(http://hqtech.nease.net/Document.htm)。其实这是一个 CTransFormFilter的空架子,只是这个Filter的Merit值非常高(0x8800000),而且支持所有的Mediatype。注册这个Filter后,当有DirectShow应用程序使用智能连接机制时,就会反反复复地使用这个Filter进行试连接,没有休止。如果你使用 Windows Media Player播放媒体文件(AVI、MPEG、WMV等等),应用程序就会阻塞住;即使是RealOne Player,在播放微软格式的文件时也会出现这种现象。 
    好了,不玩了,别把Media Player弄得太惨,毕竟对我们也没什么好处!:)期望大家已经对这个Merit有了更深的认识。接下去,把这个Filter从你的系统注销吧:regsvr32 /u yourlocalpath//HQMPKiller.ax。


    --  作者:admin 
    --  发布时间:2005-11-26 2:56:00 
    --  玩转 DVR-MS

    发布日期: 6/7/2005 | 更新日期: 6/7/2005

    Stephen Toub 
    Microsoft Corporation

    适用于: 
    Microsoft Windows XP Media Center Edition 2005 
    Microsoft DirectShow 
    DirectX 9.0 SDK

    摘要: Stephen Toub 讨论了 Windows XP Media Center 2005 生成的 DVR-MS 文件格式,介绍了 DirectShow 并展示了如何使用后者处理前者。

    下载 DVR-MS 示例 Code.msi 。

    本页内容

     
    播放 DVR-MS 文件

     
    DirectShow 和 GraphEdit 简介

     
    DirectShow 接口

     
    将编码转换为 WMV

     
    调试筛选器图形

     
    非托管资源清理

     
    将 WmvConverter 投入使用: WmvTranscoderPlugin

     
    访问 DVR-MS 元数据

     
    编辑 DVR-MS 文件

     
    小结

     
    相关书籍

     
    致谢

    几年前我拥有一台 TiVo。它已经不知藏在公寓壁橱的哪个角落了,我想现在一定是布满灰尘,诚然,就是现在我也可能这样对待它。占据电视旁宝贵空位的是一个更漂亮、更复杂的现代化软件和电子产品 — Microsoft Windows XP Media Center 2005。我的家人为该设备取了个既得体又人性化的名字 —“米老鼠”,它有许多神奇的功能。然而,当我建议我的“技术娴熟”的朋友们放弃他们现在使用的任一款数字摄像机 (DVR) 而转为使用此平台时,只要他们让我说明一个理由,我的回答都很简单:可以对录制的电视节目进行文件访问。

    DVR-MS 文件是由 Windows XP Service Pack 1 引入的流缓冲引擎(Stream Buffer Engine,SBE)创建的,Media Center 用它存储录制的电视节目。在本文中,我将向您演示如何通过托管代码使用 DirectShow 来处理和操作 DVR-MS 文件。在此过程中,我将向您介绍我为处理 DVR-MS 文件而创建的一些有用的实用工具,并为您提供您在编写自己的代码时需要的工具和库。所以,请打开 Visual Studio .NET,抓一把爆米花,享受这个过程吧。

    注 本文假定您的系统中有一个正在工作的 MPEG2 解码器,并且您使用的是 NTSC 而非 HD 内容(虽然这里讨论的大多数概念适用于 PAL 和 HD,但示例代码可能无法正确地处理这些格式)。另外,由于内容所有者或广播公司所设置的策略,一些 DVR-MS 文件受到复制保护。这种保护是在生成文件时通过检查广播公司的复制保护标志 (CGMS-A) 确定的,它会限制您访问特定 DVR-MS 文件的方式和时间。例如,在收费台(如 HBO)录制的电影可能是加密的,因此本文描述的技术就不适用了。最后,与本文相关联的代码示例和应用程序是针对 .NET Framework 1.1 编译的。然而,默认情况下 Windows XP Media Center 2005 并没有附带安装 .NET Framework 1.1,而是安装 1.0。因此,要在您的 Media Center 中使用这些示例,您必须安装 .NET Framework 1.1(可通过 Windows Update 获得)或者重新编译该示例以适用 .NET Framework 1.0。


    --  作者:admin 
    --  发布时间:2005-11-26 2:56:00 
    -- 

    播放 DVR-MS 文件

    谈到视频文件时,播放或许是可以执行的最重要的操作,所以我将从此入手。在您自己的应用程序中可以有多种播放 DVR-MS 文件的方式,这里我将演示其中的一些。为此,我创建了一个简单的应用程序(如图 1 所示),您可以在与本文有关的代码下载中获得。

    图 1. 播放 DVR-MS 文件的示例应用程序

    播放 DVR-MS 文件的第一种方式也是最简单的方式是,使用 System.Diagnostics.Process 类来执行它。由于Process.Start 包装了来自 shell32.dll 的 ShellExecuteEx 非托管函数,因此这种方式利用了与从 Windows Explorer 双击一个文件相同的功能来播放 DVR-MS 文件:

    private void btnProcessStart_Click(object sender, System.EventArgs e){  Process.Start(txtDvrmsPath.Text);}

    这也意味着,视频将在一个独立的进程中播放,这个进程在 DVR-MS 文件的任何默认处理程序中运行;对于大多数机器和我的机器来说,它就是 Windows Media Player(我使用 Windows Media Player 10,如果您没有,我建议您从 http://www.microsoft.com/windows/windowsmedia/mp10/default.aspx 免费升级到该版本)。当然,Process.Start 有另一个同时接受可执行路径和参数的重载,可以使用它在任何您想要的播放机中启动 DVR-MS 文件,不管它是否是 .dvr-ms 扩展名的默认处理程序:

    private void btnProcessStart_Click(object sender, System.EventArgs e){    Process.Start(    @"c://Program Files//Windows Media Player//wmplayer.exe",    "//"" + txtDvrmsPath.Text + "//"");}

    您应该注意到,当这样做时,有必要对 DVR-MS 文件的路径加上引号(正如此处名为 txtDvrmsPath 的 TextBox的内容所提供的),因为要使用的内容是 wmplayer.exe 的一个命令行参数。否则,路径中的任何空格都会使路径被分隔并解释为多个参数。

    Process.Start 返回一个代表启动进程的 Process 实例,这意味着您可以利用 Process 提供的功能来与 Windows Media Player 进一步交互。例如,在您的应用程序中,您可能想先等待视频停止再让用户继续,可以使用 Process.WaitForExit 方法来完成这样的任务:

    private void btnProcessStart_Click(object sender, System.EventArgs e){  using(Process p = Process.Start(txtDvrmsPath.Text))  {    p.WaitForExit();  }}

    当然,这只是等待 Media Player 关闭,而不是像初始请求那样播放您指定的文件,因为您的应用程序没有真正的视图可以查看 Media Player 执行的内容。当打开 Media Player 时,按上述方法编码也会冻结应用程序的 GUI,这个问题可以通过订阅 Process 的 Exited 事件加以解决,而不是用 WaitForExit 方法阻止。

    总而言之,该解决方案编码简单方便,但非常不灵活,而且是在应用程序的外部播放视频。它可能只在以下情况下才适用,您想允许用户查看指定的文件,不过是在应用程序不必关心视频内容而且应用程序根本不与视频交互的情况下查看。例如,如果您的应用程序是一个下载代理,而且您想允许用户查看已经复制到本地的视频文件,则可能适合采用这种方式。


    --  作者:admin 
    --  发布时间:2005-11-26 2:57:00 
    -- 

    由于我们知道 Windows Media Player 可以播放 DVR-MS 文件,因此对于大多数情况,更好的解决方案是在应用程序中宿主 Windows Media Player ActiveX 控件的一个实例。在 Visual Studio .NET 中,只需右键单击工具箱,选择添加控件并选择 Windows Media Player COM 控件。这样它就会出现在工具箱中,如图 2 所示。

    图 2. 工具箱中的 Windows Media Player ActiveX 控件

    当窗体中有一个 ActiveX 控件的实例时,让它播放 DVR-MS 文件就只需设置播放器的 URL 属性:

    player.URL = txtDvrmsPath.Text;

    在我的示例应用程序中,我选择让它更进一步。我创建了一个 System.Windows.Forms.Panel ,它位于想要显示视频的窗体中。当用户请求使用 Media Player 播放选定的视频时,我就新建一个 Media Player 控件的实例,将它添加到 Panel 的子控件集合中,使其保持在最大化,并设置其 URL 属性。这种方案允许我完全控制 Media Player 的生存期,而且可以轻松管理它在窗体中的位置,而不用担心它的绝对定位值(这种方案也使演示播放视频的其他方法变得轻松,稍后您将看到)。正在使用的这种方案的屏幕快照如图 3 所示,下面显示的是我使用的代码:

    private void btnWmp_Click(object sender, System.EventArgs e){  AxWindowsMediaPlayer player = new AxWindowsMediaPlayer();  pnlVideo.Controls.Add(player);  player.Dock = DockStyle.Fill;  player.PlayStateChange +=     new _WMPOCXEvents_PlayStateChangeEventHandler(      player_PlayStateChange);   player.URL = txtDvrmsPath.Text;}private void player_PlayStateChange(  object sender, _WMPOCXEvents_PlayStateChangeEvent e){  AxWindowsMediaPlayer player = (AxWindowsMediaPlayer)sender;  if (e.newState == (int)WMPLib.WMPPlayState.wmppsMediaEnded ||    e.newState == (int)WMPLib.WMPPlayState.wmppsStopped)  {    player.Parent = null; // removes the control from the panel    ThreadPool.QueueUserWorkItem(      new WaitCallback(CleanupVideo), sender);  }} private void CleanupVideo(object video){  ((IDisposable)video).Dispose();}

    图 3. 使用 WMP 控件的嵌入式 DVR-MS 播放

    要阻止显示 Media Player 工具栏,您可以更改控件的 uiMode 属性:

    player.uiMode = "none";

    要在用户右键单击控件时阻止显示 Media Player 上下文菜单,可以将其 enableContextMenu 属性设置为 false:

    player.enableContextMenu = false;

    您将注意到,在播放 DVR-MS 文件的前一刻,我为播放器的 PlayStateChange 事件注册了一个事件处理程序。这可以使我在播放停止时从 Panel 删除播放器。在 PlayStateChange 事件的处理程序中,我检查播放是否结束,如果结束,就将播放器从其父控件(面板)删除,并将一个工作项排入 .NET ThreadPool 队列中。这个工作项的作用只是处置播放器控件。我是在后台线程中进行此次处置的,因为无法在 PlayStateChange 事件处理程序中直接处置。在此事件处理程序中处置控件会在控件本身中引发异常,因为事件处理程序是在控件中引发的,控件在执行完我的处理程序之后还需要进行更多的处理。在处理程序中处置播放器控件会导致功能被破坏,所以我让该操作在事件处理程序完成之后稍微延迟一会,以便留出必要的时间。您将看到,在使用所演示的下一个播放机制时,就需要用到同一技术。

    宿主 Windows Media Player ActiveX 控件有许多好处。它使用起来非常方便,而且提供了大量的功能。然而,Windows Media Player 使用 DirectX(特别是 DirectShow)来播放 DVR-MS 文件(本文后面我将更详细地讨论 DirectShow)。您不是依赖 Windows Media Player 与 DirectX 交互,而是在您的应用程序中使用 Managed DirectX,完全跳过 Windows Media Player。

    在写作本文时 Managed DirectX 的最新版本是 DirectX 9.0 SDK Update February 2005 下载 的一部分。(要获得本文后面介绍的内容,您还需要 February 2005 Extras 下载 。)此 SDK 在您的全局程序集缓存 (GAC) 中安装了 AudioVideoPlayback.dll 程序集,使其可用于您的应用程序(DirectX 运行库安装也安装了此 DLL 以使您的最终用户可以访问它)。AudioVideoPlayback 是一个高级包装,它含有您在 .NET 应用程序中播放视频和音频文件所需要的最少的 DirectShow 功能。

    有了 Windows Media Player ActiveX 控件后,使用 AudioVideoPlayback 变得非常简单。

    private void btnManagedDirectX_Click(object sender, System.EventArgs e){  Video v = new Video(txtDvrmsPath.Text);  Size s = pnlVideo.Size;  v.Owner = pnlVideo;  v.Ending += new EventHandler(v_Ending);  v.Play();  pnlVideo.Size = s;}private void v_Ending(object sender, EventArgs e){  ThreadPool.QueueUserWorkItem(    new WaitCallback(CleanupVideo), sender);}private void CleanupVideo(object video){  ((IDisposable)video).Dispose();}

    这段代码首先实例化一个新的 Microsoft.DirectX.AudioVideoPlayback.Video 对象,然后将要播放的 DVR-MS 文件的路径提供给它。当播放一段 Video 时,它会自动将自身的大小(更具体地说是将它的所有者控件)调整为所播放视频的合适大小;为了解决这个问题,我存储了父面板控件的原始大小,这样在开始播放后就可以重置其大小。就像处理 ActiveX 控件那样,我注册了一个要在播放停止时激发的事件处理程序,然后播放视频。当播放结束时,我将一个工作项排入要处置 Video 对象的 ThreadPool 队列中,如同使用 ActiveX 控件一样(原因也相同)。当您不再使用 Video 对象时,对其进行处置是非常重要的;否则会浪费大量非托管资源,而且由于此对象有一个非常小的托管占地,垃圾回收器 (GC) 没有重大的动因可以及时进行回收,这样将使这些非托管资源的分配情况不明,除非您手动通过 IDisposable 处置。图 4 中的屏幕快照演示了 AudioVideoPlayback 功能的使用。

    图 4. 采用 AudioVideoPlayback 的嵌入式播放

    当然,虽然 AudioVideoPlayback 是一个高级 DirectShow 包装,但并不意味着您不能创建自己的托管包装(实际上,在本文后面我们将这样做)。创建托管包装的最简单方式是使用 tlbimp.exe(或者采用类似的做法 — 使用 Visual Studio .NET 的 COM 类型库导入功能。Visual Studio .NET 和 tlbimp.exe 都依赖于 Framework 中同样的库执行导入)。


    --  作者:admin 
    --  发布时间:2005-11-26 2:57:00 
    -- 

    DirectShow 运行库的核心库是 quartz.dll,位于 %windir%//system32//quartz.dll。它包含用于音频和视频播放的最重要的 COM 接口和 coclass,本文后面将对此进行更加详细的讨论。在 quartz.dll 上运行 tlbimp.exe 会产生一个 interop 库 — Interop.QuartzTypeLib.dll(此程序集的描述信息为“ActiveMovie control type library”,因为 DirectShow 的前身名为 ActiveMovie),并公开 FilgraphManagerClass (筛选器图形管理器)和IVideoWindow 接口。要播放视频,您只需创建该图形管理器的一个新实例并使用 RenderFile 方法,在 DVR-MS 文件路径中传送,以便初始化该对象以进行播放。然后可以使用由 FilgraphManagerClass 实现的IVideoWindow 接口来控制播放选项,例如所有者窗口、视频在父窗口中的位置,以及视频窗口的标题。要开始播放,可以使用 Run 方法。WaitForCompletion 方法可以用于等待视频停止播放(或者,可以指定一个正的毫秒数,作为要等待的最长时间),Stop 方法可以用于暂停播放。要销毁该对象并释放用于播放的所有非托管资源(包括播放窗口本身),System.Runtime.InteropServices.Marshal 类及其 ReleaseComObject 方法就会派得上用场了。使用 quartz.dll 的屏幕快照如图 5 所示。

    private void btnQuartz_Click(object sender, System.EventArgs e){  FilgraphManagerClass fm = new FilgraphManagerClass();  fm.RenderFile(txtDvrmsPath.Text);  IVideoWindow vid = (IVideoWindow)fm;  vid.Owner = pnlVideo.Handle.ToInt32();  vid.Caption = string.Empty;  vid.SetWindowPosition(0, 0, pnlVideo.Width, pnlVideo.Height);  ThreadPool.QueueUserWorkItem(new WaitCallback(RunQuartz), fm);}private void RunQuartz(object state){  FilgraphManagerClass fm = (FilgraphManagerClass)state;  fm.Run();  int code;  fm.WaitForCompletion(Timeout.Infinite, out code);  fm.Stop();  while(Marshal.ReleaseComObject(fm) > 0);}

    图 5. 使用 quartz.dll 的嵌入式播放

    我刚刚向您介绍了一些在自己的应用程序中播放 DVR-MS 文件的方法。虽然我讨论了多个播放 DVR-MS 文件的方法(而且我还没列举完),但所有这些方法都要依赖于 DirectShow 才有播放功能。因此,我们将简要介绍一下 DirectShow(或者让那些具有 DirectShow 经验的人重温一下)。

     返回页首

    DirectShow 和 GraphEdit 简介

    在本质上,使用 DirectShow 处理视频文件的应用程序是通过一组称为筛选器的组件完成的。一个筛选器通常只对多媒体数据流执行一种操作。这样的筛选器很多,每个筛选器执行不同的任务,例如读取 DVR-MS 文件、写出 AVI 文件、对 MPEG-2 压缩视频进行解码、将视频和音频呈现到视频卡和声卡上,等等。这些筛选器的实例可以连接在一起并组合成一个筛选器图形,然后由 DirectShow 筛选器图形管理器组件进行管理(在前面介绍 quartz.dll 时,您已简要地对其进行了了解)。这些图形是定向的,也是非循环的,这意味着两个筛选器之间的特定连接只允许数据朝一个方向流动,而且只能流经特定筛选器一次。这种数据流程称为流 (stream),而筛选器则用来处理这些流。筛选器是通过它们公开的针 (pin) 连接到其他筛选器的,因此,一个筛选器的输出针连接到另一个筛选器的输入针,并按从前者发送到后者的方式发送数据流。

    为了对此进行演示并显示本文中所使用的图形,我使用了 DirectX SDK 中一个名为 GraphEdit 的实用工具。GraphEdit 可以用来使筛选器图形可视化,当要确定如何构建用于特定目的的图形以及调试您所构建的图形时,这个功能就能派上用场。稍后,我将介绍如何使用 GraphEdit 来对在您的应用程序中运行的筛选器图形进行连接和可视化。

    现在,我们运行 GraphEdit。在“File”菜单下,选择“Render Media File”,然后选择本地可用的任何有效的 DVR-MS 文件(请注意,您可能需要在“Open File”对话框中将筛选器扩展名更改为“All Files”,而不是“All Media Files”,因为最近发布的 GraphEdit 版本并没有将 .dvr-ms 扩展名归类为媒体文件)。您应该能够看到一个图形,它类似于图 6 所示的图形。

    图 6. GraphEdit 准备播放 DVR-MS 文件

    此时,GraphEdit 已构造了一个筛选器图形,它能够播放选定的 DVR-MS 文件。这些蓝框中的每一个都是一个筛选器,箭头显示每个筛选器上的输入和输出针如何互相连接以形成图形。图形中的第一个筛选器是 StreamBufferSource 筛选器的实例,它由 Windows XP SP1 及更高版本的 %windir%//system32//sbe.dll 库公开。选择这个筛选器是因为它在注册表中配置为 .dvr-ms 扩展名的源筛选器 (HKCL//Media Type//Extensions//.dvr-ms//Source Filter)。它的作用是从磁盘中读取一个文件,并将该文件的数据以流的形式发送到图形的其他部分。它从一个 DVR-MS 文件提供三个流。


    --  作者:admin 
    --  发布时间:2005-11-26 2:58:00 
    -- 

    第一个是音频流。如果您检查第一个针的针属性(DVR Out - 1,可以通过右键单击 GraphEdit 中的针来访问针属性),您可以发现该针的主要类型是 Audio,而其子类型是 Encrypted/Tagged,这意味我们在对该数据进行任何操作之前必须先对它进行解密和/或取消标记。这个过程是由 Decrypter/Detagger 筛选器(由 %windir%//system32//encdec.dll 公开)处理的。Decrypter/Detagger 将加密/带标记的音频流作为输入,然后发出 MPEG-1 音频流(对于高清晰度的内容则输出 dolby-AC3 流),这一点您可以通过检查该筛选器的 In(Enc/Tag) 和 Out 针加以验证。这里将音频发送到 MPEG Audio Decoder 筛选器(由 quartz.dll 公开),通过它将音频解压缩为脉冲编码调制 (PCM) 音频流。音频流的最后一个筛选器 DirectSound Audio Renderer(也由 quartz.dll 公开)接收此 PCM 音频数据并在计算机的声卡上播放。

    DVR-MS 源筛选器提供的第二个流包含所录制的电视节目的闭合字幕数据。和音频流一样,闭合字幕流也是经过加密/标记的,所以它必须首先通过 Decrypter/Detagger 筛选器。如果查看此筛选器的 Out 针,您会发现其主要类型是 AUXLine21Data,而其子类型是 Line21_BytePair。电视节目中的闭合字幕是作为电视图像的一部分发送的,并专门编码到图像的 line 21 中。

    DVR-MS 源筛选器发出的第三个流是视频内容 (video feed)。与音频和闭合字幕数据一样,这个流也是经过加密/标记的,所以它必须首先通过 Decrypter/Detagger 筛选器。Decrypter/Detagger 筛选器的输出是 MPEG-2 视频流,所以它必须先通过 MPEG-2 视频解码器才能呈现视频。Microsoft 没有在 Windows 中附带 MPEG-2 解码器,所以系统中必须有可用的第三方解码器才能播放。解码后的视频流再送到默认的视频呈现程序(由 quartz.dll 公开)。

    单击图形上方的绿色播放按钮就会出现一个标题为 ActiveMovie Window 的新窗口并在该窗口中播放 DVR-MS 文件。请注意,由于闭合字幕 Decrypt/Tag Out 针没有连接到任何地方,因此在呈现视频时没有用到闭合字幕数据。您可以通过修改图形对此进行更改。实际做法是,首先删除默认的视频呈现程序(单击该筛选器并按“Delete”键),因为该呈现程序不能处理多路输入。具体来说,我们需要这样的呈现程序:它可以显示视频流,并能将包含呈现的闭合字幕数据的位图覆盖其上。如何从 Decrypter/Detagger 筛选器获取 line 21 字节对,将其作为位图呈现出来呢?Windows 实际上附带了一个正好可以完成此任务的 DirectShow 筛选器。使用“Graph”菜单下的“Insert Filters...”命令,展开树视图中的 DirectShow 筛选器节点并选择“Video Mixing Renderer 9”筛选器。单击“insert”按钮将此筛选器的实例添加到图形中,然后关闭“insert filters”对话框。现在,Video Mixing Renderer 9 筛选器成为图形的一部分了,但没有连接到任何地方,也就不能使用(实际上,如果您现在单击“play”按钮,则只播放音频,因为视频流没有连接到呈现程序)。单击 MPEG-2 解码器上的 Video Output 针,并将它拖到呈现程序的 VMR Input0 针上(请注意,如果您使用的解码器不是 NVDVD,则视频输出针的名称可能不同,但概念是一样的)。如果您现在播放图形,则会看到输出与使用默认视频呈现程序播放时基本一致。然而,您将看到,此时呈现程序筛选器公开了多个输入针(实际上,筛选器可以根据连接到它们的其他筛选器动态更改公开的针)。我们可以将闭合字幕 Decrypter/Detagger 筛选器的 Out 针连接到呈现程序的 VMR Input1 针上,以此利用这一特性。GraphEdit 会自动插入一个 Line 21 Decoder 2 筛选器,将 Decrypter/Detagger 筛选器连接到解码器筛选器,并将解码器筛选器连接到呈现程序筛选器。现在,您应该能看到如图 7 所示的图形。当您播放此图形时,您将看到闭合字幕像您期望的那样,以文本的形式出现在视频前。

    图 7. 将闭合字幕合并到视频显示中

    此时,对 DirectShow 不熟悉的读者可能会产生疑惑:是如何发现 Line 21 Decoder 2 筛选器的?为什么一开始只需使用 GraphEdit 的 Render Media File 操作就能构造出整个图形呢?GraphEdit 依赖 IGraphBuilder 接口提供的功能来查找和选择合适的筛选器,并在需要时将它们互连(IGraphBuilder 是由我们在介绍如何播放 DVR-MS 文件时简要提到的 FilgraphManager 组件实现的,实际上我们使用的 RenderFile 方法就是 IGraphBuilder接口的一部分)。


    --  作者:admin 
    --  发布时间:2005-11-26 2:58:00 
    -- 

    这种用于自动构建筛选器图形的机制称为 Intelligent Connect。由于您并不真的需要知道 Intelligent Connect 的具体内容(除非您正在实现自己的筛选器并想让它们可以自动构建图形),因此在这里此主题我不想介绍得太多,而是让您参考 DirectX SDK 中该主题的详细文档。然而,简单地说,RenderFile 方法是一个简单的包装,它包装了 IGraphBuilder 中的另外两个方法:AddSourceFilter 和 Render 。RenderFile 首先调用AddSourceFilter ,对于本地文件,它只需在注册表中查找正在播放的文件的扩展名所必需的源筛选器的类型,将适当的筛选器实例添加到筛选器图形中,并对它进行配置以使其指向指定的源文件。对于此源筛选器的每个输出针,RenderFile 再调用 Render 方法,该方法试图查找从此针到图形中的呈现程序的一条路径。如果该针实现了 IStreamBuilder 接口,则 Render 只是委托该实现,将所有细节都交给该筛选器的实现。否则,Render会试图查找此针可以连接的筛选器。为此,它会查找在图形构建过程前期可能缓存的缓存筛选器,查找已经成为图形的一部分且有未连接的输入针的任何筛选器,并使用 IFilterMapper 接口查找注册表中兼容的筛选器类型。如果找到了一个筛选器,则它会再对这个新的筛选器重复此过程,直到到达呈现筛选器,此时就成功地停止。如果没有找到筛选器,则 Intelligent Connect 构建图形未成功。这就是依赖 Intelligent Connect 的一个缺点:它并非始终有效。另外,如果您的机器上安装了新的筛选器,则 Intelligent Connect 可能会选择这些新的筛选器,而不是您当前期望在应用程序中使用的筛选器。因此,您在设计时可能要选择避免这种情况(我后面将要介绍,如果您确切地知道想在图形中使用哪些筛选器,则显式构建图形而不使用 Intelligent Connect 是很容易的)。

    既然您对 DirectShow 已有所了解,我们将要以编程方式使用它,以便对 DVR-MS 文件进行许多很合适的操作。毕竟,一旦 DVR-MS 源筛选器加载到图形中,我们就可以像处理其他音频和视频数据流那样处理来自 DVR-MS 的数据,操作它们的方法是无限的。


    --  作者:admin 
    --  发布时间:2005-11-26 2:58:00 
    -- 

    DirectShow 接口

    然而,我们首先需要的是能够以编程方式处理 DirectShow。对于非托管代码,这可能是立即可行的,因为 SDK 包含了通过 C++ 访问 DirectShow 库所需要的所有头文件。对于托管代码,问题就有些棘手。虽然 Managed DirectX 确实包含前面讨论的 AudioVideoPlayback.dll 库,但该库级别很高,它提供 Video 和 Audio 级别的抽象,而我们需要的是能够在筛选器和针级别对筛选器图形进行操作。虽然我觉得这个问题将来会得到改善,但至少当前版本的 Managed DirectX 对我们爱莫能助。

    quartz.dll 是什么?quartz.dll 的类型库公开了一些我们需要的功能,这里列出所公开接口的完整列表

    [此贴子已经被作者于 2005-11-26 3:03:44编辑过]


    --  作者:admin 
    --  发布时间:2005-11-26 2:59:00 
    -- 

    接口 
    描述

    IAMCollection

    筛选器图形对象集合,例如筛选器或针。

    IAMStats

    允许应用程序从图形管理器中检索性能数据。筛选器可以使用此接口记录性能数据。

    IBasicAudio

    允许应用程序控制音频流的音量和平衡。

    IBasicVideo

    允许应用程序设置视频属性,例如目标矩形和源矩形

    IBasicVideo2

    从 IBasicVideo 接口派生,为应用程序提供了一个附加方法,通过它可以检索视频流的首选纵横比。

    IDeferredCommand

    允许应用程序取消或修改该应用程序先前使用 IQueueCommand 接口排入队列的图形-控制命令。

    IFilterInfo

    管理筛选器的信息并提供访问筛选器和表示筛选器上的针的 IPinInfo 接口。

    IMediaControl

    提供方法来控制经过筛选器图形的数据流。它包含运行、暂停和停止图形的方法。

    IMediaEvent

    包含用来检索事件通知和用于重写筛选器图形管理器的默认事件处理的方法。

    IMediaEventEx

    从 IMediaEvent 派生并添加方法来启用一个应用程序窗口,以便在事件发生时接收消息。

    IMediaPosition

    包含用于查找流中一个位置的方法。

    IMediaTypeInfo

    包含用于检索针连接的媒体类型的方法。

    IPinInfo

    包含用于检索针信息和连接针的方法。

    IQueueCommand

    允许应用程序预先将图形-控制命令排入队列。

    IRegFilterInfo

    提供对 Windows 注册表中的筛选器的访问,以及向筛选器图形中添加已注册的筛选器。

    IVideoWindow

    包含用于设置窗口所有者、窗口的位置和尺寸及其他窗口属性的方法。

    [此贴子已经被作者于 2005-11-26 3:12:42编辑过]


    --  作者:admin 
    --  发布时间:2005-11-26 3:13:00 
    --   
    这确实是个很好的开头,但它没有为我们提供一些处理图形和筛选器的最重要的接口。例如,手动构造图形比较常用的接口之一,IGraphBuilder 接口,并没有包括在内。表示特定筛选器实例和提供对其针访问的IBaseFilter 接口也没有包括在内。下表列出了在本文中要完成图形需要访问的主要接口:


    --  作者:admin 
    --  发布时间:2005-11-26 3:15:00 
    -- 

    接口 
    描述

    IBaseFilter

    提供用于控制筛选器的方法。应用程序可以使用此接口枚举针和查询筛选器信息。

    IConfigAsfWriter2

    提供用于获取和设置 WM ASF Writer 筛选器写文件要使用的高级流格式(Advanced Streaming Format,ASF)配置文件的方法和用于支持 Windows Media Format 9 Series SDK 中的新功能(例如双向编码和对反交错视频的支持)的方法。

    IFileSinkFilter

    在将媒体流写入文件的筛选器上实现。

    IFileSourceFilter

    在从文件读媒体流的筛选器上实现。

    IGraphBuilder

    提供方法来支持应用程序构建筛选器图形。

    IMediaControl

    提供方法来控制数据流经筛选器图形的流程。它包括用于运行、暂停和停止图形的方法。

    IMediaEvent

    包含用于检索事件通知和重写筛选器图形管理器的默认事件处理的方法。

    IMediaSeeking

    包含用于查询当前位置和查找流中的特定位置的方法。

    IWmProfileManager

    用于创建配置文件、加载现有的配置文件和保存配置文件。

    另外,我还需要显式实例化各个 COM 类,下面展示了其中最重要的一些类,以及它们的类 ID 和对每个类的描述:

    类 
    类 ID 
    描述

    筛选器图形管理器

    E436EBB3-524F-11CE-9F53-0020AF0BA770

    构建和控制筛选器图形。此对象是 DirectShow 中的中心组件。

    Decrypter/Detagger 筛选器

    C4C4C4F2-0049-4E2B-98FB-9537F6CE516D

    有条件地解密由 Encrypter/Tagger 筛选器加密的示例。输出类型与 Encrypter/Tagger 筛选器接收到的原始输入类型相匹配。

    WM ASF Writer 筛选器

    7C23220E-55BB-11D3-8B16-00C04FB6BD3D

    接受数量可变的输入流并创建高级流格式 (ASF) 文件。


    --  作者:admin 
    --  发布时间:2005-11-26 3:16:00 
    -- 

    正如 Eric Gunnerson 在关于 DirectShow 和 C# 的 his blog entry 中指出的,一种快捷简便的导入接口的方法是使用 DirectX SDK 附带的 DirectShow 接口定义语言(Interface Definition Language,IDL)文件。这些文件包含了 COM 接口定义,我对其中的大部分接口都很感兴趣。我可以创建自己的 IDL 文件(它的创作是为了产生一个类型库),然后通过 Microsoft 接口定义语言 (MIDL) 编译器 (midl.exe) 运行它。这将产生一个类型库,然后我再使用 .NET Framework tool Type Library Importer (tlbimp.exe) 将它转换成托管程序集。

    遗憾的是,Eric 也指出,它不是一个完美的解决方案。首先,随 DirectX SDK 附带的 IDL 文件并没有描述我需要的所有接口,例如 IMediaEvent 和 IMediaControl 。其次,即使我需要的所有接口都描述了,但通常需要对 interop 签名的创建进行更多控制,而不只是 tlbimp.exe 所提供的控制。例如,如果在图形运行完成之前用户指定的时间到期,则 IMediaEvent.WaitForCompletion (本文后面将会介绍)会返回一个 E_ABORT HRESULT;它将转换成在 .NET 中引发的异常,如果您在轮询循环中要频繁调用 WaitForCompletion (我就打算这样做),则这样做就不合适。另外,IDL 类型和托管类型之间并不是一对一的映射;实际上,存在这样的情况,类型可能根据使用它的上下文不同而进行不同的封送处理。例如,在 DirectX SDK 的 axcore.idl 文件中,IEnumPins 接口公开了以下方法:

    HRESULT Next(  [in] ULONG cPins,            // Retrieve this many pins.  [out, size_is(cPins)] IPin ** ppPins,  // Put them in this array.  [out] ULONG * pcFetched         // How many were returned?);

    当它编译成类型库并由 tlbimp.exe 进行转换时,产生的程序集包含以下方法:

    void Next(  [In] uint cPins,   [Out, MarshalAs(UnmanagedType.Interface)] out IPin ppPins,   [Out] out uint pcFetched);

    虽然非托管的 IEnumPins::Next 可以被任何正整数值的 cPins 调用,但如果调用托管版本用的 cPins 值不是 1,则会产生错误,因为 ppPins 不是 IPin 实例数组,而是单个 IPin 实例的引用。


    --  作者:admin 
    --  发布时间:2005-11-26 3:16:00 
    -- 

    基于所有这些原因,以及 DirectShow 接口相对简单,我选择手动用 C# 实现 COM 接口 interop 定义;虽然这需要的工作更多,但它可以让您最好地控制封送内容、方式和时间(不过,请注意,在创建这些手动编码的 interop 定义时,采用 tlbimp.exe 生成的 MSIL 是一个很好的起点,或者更好的方式 — 采用这些导入类型库的反编译 C# 实现,可以使用 Lutz Roeder 的 .NET 发送程序生成它,这个程序可以从 http://www.aisto.com/roeder/dotnet/ 获得)。在与本文有关的代码下载中,您会发现我在本文中使用的每个非托管 DirectShow 接口都有手动编码的 C# 接口。举个例子,下面是前面讨论的 IGraphBuilder 接口的 C# 实现:

    [ComImport][Guid("56A868A9-0AD4-11CE-B03A-0020AF0BA770")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IGraphBuilder{  void AddFilter([In] IBaseFilter pFilter,     [In, MarshalAs(UnmanagedType.LPWStr)] string pName);  void RemoveFilter([In] IBaseFilter pFilter);  IEnumFilters EnumFilters();  IBaseFilter FindFilterByName(    [In, MarshalAs(UnmanagedType.LPWStr)] string pName);  void ConnectDirect([In] IPin ppinOut, [In] IPin ppinIn,     [In, MarshalAs(UnmanagedType.LPStruct)] AmMediaType pmt);  void Reconnect([In] IPin ppin);  void Disconnect([In] IPin ppin);  void SetDefaultSyncSource();  void Connect([In] IPin ppinOut, [In] IPin ppinIn);  void Render([In] IPin ppinOut);  void RenderFile(    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFile,    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrPlayList);  IBaseFilter AddSourceFilter(    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFileName,    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFilterName);  void SetLogFile(IntPtr hFile);  void Abort();  void ShouldOperationContinue();}

    然后就可以通过我的 IGraphBuilder 接口来转换和使用筛选器图形管理器组件的实例。那么,如何获取筛选器图形管理器组件的实例呢?我使用了如下代码:

    public class ClassId{  public static readonly Guid FilterGraph =     new Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770");  public static readonly Guid WMAsfWriter =     new Guid("7C23220E-55BB-11D3-8B16-00C04FB6BD3D");  public static readonly Guid DecryptTag =     new Guid("C4C4C4F2-0049-4E2B-98FB-9537F6CE516D");  ...  public static object CoCreateInstance(Guid id)  {    return Activator.CreateInstance(Type.GetTypeFromCLSID(id));  }}

    在这个包装就位后,我就可以创建筛选器图形管理器的实例,配置能够播放 DVR-MS 文件的筛选器图形,以及播放文件,总共只需要五行代码:

    object filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);((IGraphBuilder)filterGraph).RenderFile(pathToDvrmsFile);((IMediaControl)filterGraph).Run();EventCode status;((IMediaEvent)filterGraph).WaitForCompletion(Timeout.Infinite, out status);

    既然我们知道如何通过托管代码使用 DirectShow,现在我们就来看看如何利用它做一些很酷的事情。


    --  作者:admin 
    --  发布时间:2005-11-26 3:17:00 
    -- 

    将编码转换为 WMV

    如果 Internet 搜索引擎能提供任何线索,则人们对 DVR-MS 文件想做的最流行的一件事情就是将它们转换成 Windows Media Video 文件。通过目前我们为处理 DVR-MS 文件和 DirectShow 而创建的框架,这个任务是很容易实现的。简单地说,我需要做的就是创建一个使用 DVR-MS 源筛选器和 WM ASF Writer 筛选器接收器(它编码并写出 WMV 文件)的图形,并在它们之间建立适当的筛选器和连接。我故意对这些中间筛选器含糊其词,因为我可以让 Intelligent Connect 替我查找和插入它们。作为说明手动进行此操作的简单性的例子,我们按照以下简单步骤在 GraphEdit 中创建适当的转换图形:

    1.

    打开 GraphEdit。

    2.

    从“Graph”菜单中选择“Insert Filters”,插入一个 DirectShow WM ASF Writer 筛选器。当提示输入一个输出文件名时,请输入目标文件的名称,以 .wmv 为扩展名。

    3.

    从“File”菜单中选择“Render Media File”,并在弹出的“Open File”对话框中选择输入的 DVR-MS 文件(再次提醒,您很可能需要将筛选器文件扩展名更改为“All Files”而不是“All Media Files”)。

    GraphEdit 将使用该图形的 RenderFile 方法来为 DVR-MS 文件添加一个源筛选器,并通过需要的一系列中间筛选器将它连接到适当的呈现程序。由于以上操作发生时 WM ASF Writer 筛选器接收器已经在图形中,因此使用 Intelligent Connect 的 RenderFile 会将流发送到该筛选器接收器上,而不是插入新的默认呈现程序筛选器。您应该能看到如图 8 所示的图形。

    图 8. 将 DVR-MS 编码转换为 WMV 的图形

    以编程方式进行这种转换是非常简单的,可以通过以下代码实现:

    // Get the filter graphobject filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);DisposalCleanup.Add(filterGraph);IGraphBuilder graph = (IGraphBuilder)filterGraph;// Add the ASF writer and set the output nameIBaseFilter asfWriterFilter = (IBaseFilter)  ClassId.CoCreateInstance(ClassId.WMAsfWriter);DisposalCleanup.Add(asfWriterFilter);graph.AddFilter(asfWriterFilter, null);IFileSinkFilter sinkFilter = (IFileSinkFilter)asfWriterFilter;sinkFilter.SetFileName(OutputFilePath, null);// Render the DVR-MS file and run the graphgraph.RenderFile(InputFilePath, null);RunGraph(graph, asfWriterFilter);

    先创建一个筛选器图形,将 WM ASF Writer 筛选器添加到其中并配置为指向适当的输出文件路径,然后将 DVR-MS 文件添加到该图形中并使用图形的 RenderFile 方法来呈现。遗憾的是,这在控制 WMV 文件编码方式上并没有提供很多灵活性。为了做到这一点,我们需要用一个配置文件配置 WM ASF Writer,这可以通过在调用RenderFile 之前插入以下代码来完成:

    // Set the profile to be used for conversionif (_profilePath != null){  // Load the profile XML contents  string profileData;  using(StreamReader reader =     new StreamReader(File.OpenRead(_profilePath)))  {    profileData = reader.ReadToEnd();  }  // Create an appropriate IWMProfile from the data  IWMProfileManager profileManager = ProfileManager.CreateInstance();  DisposalCleanup.Add(profileManager);  IntPtr wmProfile = profileManager.LoadProfileByData(profileData);  DisposalCleanup.Add(wmProfile);  // Set the profile on the writer  IConfigAsfWriter2 configWriter =    (IConfigAsfWriter2)asfWriterFilter;  configWriter.ConfigureFilterUsingProfile(wmProfile); }

    这段代码假定配置文件 PRX 文件的路径已经存储在字符串成员变量 _profilePath 中。首先,使用System.IO.StreamReader 将该配置文件的 XML 内容读到一个字符串中。然后创建 Windows Media Profile Manager(通过 IWMProfileManager 接口访问),并使用该管理器的 LoadProfileByData 方法将配置文件加载到其中。这为我们提供了一个指向所加载的配置文件的接口指针,可以用它来配置 WM ASF Writer 筛选器。WM ASF Writer 筛选器实现了 IConfigAsfWriter2 接口,它提供了 ConfigureFilterUsingProfile 方法,这个方法可以根据接口指针指定的配置文件配置编写器。

    创建和配置好图形之后,剩下的工作就是运行它,我是使用特意指定的 RunGraph 方法实现的。该方法首先获取指定图形的 IMediaControl 和 IMediaEvent 接口。它还试图获取可用于跟踪源 DVR-MS 文件处理进度的IMediaSeeking 接口。然后使用 IMediaControl 接口来运行图形,从此时开始,方法中的剩余代码仅仅是用来跟踪转换的处理进度。在图形结束运行前,代码会不断轮询 IMediaEvent.WaitForCompletion 方法,如果等待时间已到但图形还没完成运行,则该方法将返回状态代码 EventCode.None (0x0)。如果发生这种情况,则会使用 IMediaSeeking 接口来查询已经处理多少 DVR-MS 文件以及该文件的持续时间,由此我可以计算文件处理的百分比。

    当图形最终完成运行时,IMediaEvent.WaitForCompletion 会返回 EventCode.Complete (0x1),并使用IMediaControl.Stop 来停止图形。


    --  作者:admin 
    --  发布时间:2005-11-26 3:17:00 
    -- 

    protected void RunGraph( IGraphBuilder graphBuilder, IBaseFilter seekableFilter){ IMediaControl mediaControl = (IMediaControl)graphBuilder; IMediaEvent mediaEvent = (IMediaEvent)graphBuilder; IMediaSeeking mediaSeeking = seekableFilter as IMediaSeeking; if (!CanGetPositionAndDuration(mediaSeeking))  {  mediaSeeking = graphBuilder as IMediaSeeking;  if (!CanGetPositionAndDuration(mediaSeeking)) mediaSeeking = null; } using(new GraphPublisher(graphBuilder,  Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf")) {  mediaControl.Run();  try  {   OnProgressChanged(0);   bool done = false;   while(!CancellationPending && !done)   {    EventCode statusCode = EventCode.None;    int hr = mediaEvent.WaitForCompletion(     PollFrequency, out statusCode);    switch(statusCode)    {     case EventCode.Complete:      done = true;      break;     case EventCode.None:       if (mediaSeeking != null)      {       ulong curPos = mediaSeeking.GetCurrentPosition();       ulong length = mediaSeeking.GetDuration();       double progress = curPos * 100.0 / (double)length;       if (progress > 0) OnProgressChanged(progress);      }      break;     default:      throw new DirectShowException(hr, null);    }   }   OnProgressChanged(100);  }  finally { mediaControl.Stop(); } }}

    简单吧?DirectShow 是一项令人惊讶的技术。这段代码允许您将非 DRM/'d、NTSC、存储在 DVR-MS 文件中的 SD 内容转换成 WMV 文件。如果您检查本文代码下载中的文件,正如您将看到的,我已将此函数编码到一个名为Converter 的抽象基类中。一个派生类(在本例中为 WmvConverter )构建合适的图形,然后调用基类的RunGraph 方法。另外,Converter 还公开了可用于配置、监视和暂停图形流程的属性和事件,正如您在以下部分将看到的,Converter 公开了使调试图形变得更加简单的功能。


    --  作者:admin 
    --  发布时间:2005-11-26 3:19:00 
    -- 

    调试筛选器图形

    您将在 RunGraph 方法中看到,图形是在如下所示的 using 块内部运行的:

    using(new GraphPublisher(graphBuilder,  Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf")){  ... // run the graph}

    我这里使用的 GraphPublisher 类是一个自定义类,它是我为帮助调试图形而编写的。它有两个用途。第一,如果在 GraphPublisher 的构造函数的第二个参数中指定了一个文件路径,则它会将 graphBuilder 对象所表示的图形保存到该文件中(该文件应该使用 .grf 扩展名)。随后 GraphEdit 可以打开此文件,从而让您查看整个图形,如同它在发布时出现的样子。这个功能可以通过筛选器图形管理器的 IPersistStream 接口实现来使用:

    private const ulong STGM_CREATE = 0x00001000L;private const ulong STGM_TRANSACTED = 0x00010000L;private const ulong STGM_WRITE = 0x00000001L;private const ulong STGM_READWRITE = 0x00000002L;private const ulong STGM_SHARE_EXCLUSIVE = 0x00000010L;[DllImport("ole32.dll", PreserveSig=false)]private static extern IStorage StgCreateDocfile(  [MarshalAs(UnmanagedType.LPWStr)]string pwcsName,   [In] uint grfMode, [In] uint reserved);private static void SaveGraphToFile(IGraphBuilder graph, string path){  using(DisposalCleanup dc = new DisposalCleanup())  {    string streamName = "ActiveMovieGraph";    IPersistStream ps = (IPersistStream)graph;    IStorage graphStorage = StgCreateDocfile(path,      (uint)(STGM_CREATE | STGM_TRANSACTED |       STGM_READWRITE | STGM_SHARE_EXCLUSIVE), 0);    dc.Add(graphStorage);    UCOMIStream stream = graphStorage.CreateStream(      streamName, (uint)(STGM_WRITE | STGM_CREATE |       STGM_SHARE_EXCLUSIVE), 0, 0);    dc.Add(stream);    ps.Save(stream, true);    graphStorage.Commit(0);  }}

    然而,GraphPublisher 的主要目的和它在 using 块中使用的原因是将实时图形发布到 GraphEdit。GraphEdit 允许您连接到另一个流程所公开的远程图形,只要该图形已经发布到运行中对象表 (ROT) — 一个用作跟踪运行对象的全局可访问的查找表。GraphEdit 不仅可以让您在另一个流程中查看和检查一个实时筛选器图形,它还常常允许您对其加以控制。

    该图形发布到 ROT 是使用以下代码完成的:

    private class RunningObjectTableCookie : IDisposable{  private int _value;  private bool _valid;  internal RunningObjectTableCookie(int value)  {    _value = value;    _valid = true;  }  ~RunningObjectTableCookie() { Dispose(false); }  public void Dispose()  {    GC.SuppressFinalize(this);    Dispose(true);  }  private void Dispose(bool disposing)  {    if (_valid)    {      RemoveGraphFromRot(this);      _valid = false;      _value = -1;    }  }  internal bool IsValid   {     get { return _valid; } set { _valid = value; }   }}private static RunningObjectTableCookie AddGraphToRot(  IGraphBuilder graph){  if (graph == null) throw new ArgumentNullException("graph");  UCOMIRunningObjectTable rot = null;  UCOMIMoniker moniker = null;  try   {    // Get the ROT    rot = GetRunningObjectTable(0);    // Create a moniker for the graph    int pid;    using(Process p = Process.GetCurrentProcess()) pid = p.Id;    IntPtr unkPtr = Marshal.GetIUnknownForObject(graph);    string item = string.Format("FilterGraph {0} pid {1}",       ((int)unkPtr).ToString("x8"), pid.ToString("x8"));    Marshal.Release(unkPtr);    moniker = CreateItemMoniker("!", item);        // Registers the graph in the running object table    int cookieValue;    rot.Register(ROTFLAGS_REGISTRATIONKEEPSALIVE, graph,       moniker, out cookieValue);    return new RunningObjectTableCookie(cookieValue);  }  finally  {    // Releases the COM objects    if (moniker != null)       while(Marshal.ReleaseComObject(moniker)>0);     if (rot != null) while(Marshal.ReleaseComObject(rot)>0);   }}private static void RemoveGraphFromRot(RunningObjectTableCookie cookie){  if (!cookie.IsValid) throw new ArgumentException("cookie");  UCOMIRunningObjectTable rot = null;  try   {    // Get the running object table and revoke the cookie    rot = GetRunningObjectTable(0);    rot.Revoke(cookie.Value);    cookie.IsValid = false;  }  finally  {    if (rot != null) while(Marshal.ReleaseComObject(rot)>0);   }}private const int ROTFLAGS_REGISTRATIONKEEPSALIVE  = 1;[DllImport("ole32.dll", ExactSpelling=true, PreserveSig=false)]private static extern UCOMIRunningObjectTable GetRunningObjectTable(  int reserved);[DllImport("ole32.dll", CharSet=CharSet.Unicode,   ExactSpelling=true, PreserveSig=false)]private static extern UCOMIMoniker CreateItemMoniker(  [In] string lpszDelim, [In] string lpszItem);

    在其构造函数中,GraphPublisher 使用 AddGraphToRot 将图形添加到 ROT 中,并存储产生的 cookie。在其IDisposable.Dispose 方法中,GraphPublisher 通过将存储的 cookie 传递到 RemoveGraphFromRot 来将图形从 ROT 中删除。


    --  作者:admin 
    --  发布时间:2005-11-26 3:21:00 
    -- 

    非托管资源清理

    当资源使用完毕后,尽早将它们释放是非常重要的。当使用处理大量音频和视频资源的 DirectShow COM 对象时,这一点尤其重要。可以使用 Marshal.ReleaseComObject 方法来强制处置 COM 对象,此方法会减少所提供的运行时可调用包装的引用计数。当引用数到达零时,运行库会释放它在非托管 COM 对象上的所有引用。(有关 Marshal.ReleaseComObject 的更多信息,请参见该方法的 MSDN 文档 。)对于使用的每个 COM 对象,我不是将我的代码随便放在 try/finally 块中,而是创建一个名为 DisposalCleanup 的助手类,它可以简化 COM 对象的生存期管理:

    public class DisposalCleanup : IDisposable{  private ArrayList _toDispose = new ArrayList();  public void Add(params object [] toDispose)  {    if (_toDispose == null)       throw new ObjectDisposedException(GetType().Name);    if (toDispose != null)    {      foreach(object obj in toDispose)      {        if (obj != null && (obj is IDisposable ||           obj.GetType().IsCOMObject || obj is IntPtr))        {          _toDispose.Add(obj);        }      }    }  }  void IDisposable.Dispose()  {    if (_toDispose != null)    {      foreach(object obj in _toDispose) EnsureCleanup(obj);      _toDispose = null;    }  }  private void EnsureCleanup(object toDispose)  {    if (toDispose is IDisposable)     {      ((IDisposable)toDispose).Dispose();    }    else if (toDispose is IntPtr) // IntPtrs must be interface ptrs    {      Marshal.Release((IntPtr)toDispose);    }    else if (toDispose.GetType().IsCOMObject)     {      while (Marshal.ReleaseComObject(toDispose) > 0);    }  }}

    这里一个重要的方法是 EnsureCleanup ,它是通过 DisposalCleanup 的 IDisposable.Dispose 方法调用的。通过使用其 Add 方法来调用添加到 DisposalCleanup 中的每个对象,EnsureCleanup 调用了一个IDisposable 对象上的 Dispose 、一个 COM 对象上的 Marshal.ReleaseComObject 和一个接口指针上的Marshal.Release 。通过这些,我的代码只需将使用许多 COM 对象的代码块放在一个创建了新的DisposalCleanup 的 using 块中,将任何 COM 对象或接口添加到 DisposalCleanup 实例中,并在 using 块结束时调用 DisposalCleanup 的 IDisposable.Dispose 方法来释放所有使用过的资源。我的 Converter 基类实现了此方案,并通过一个受保护的 DisposalCleanup 属性公开了构造的 DisposalCleanup 。

    public object Convert() {   _cancellationPending = false;  try  {    object result;    using(_dc = new DisposalCleanup())    {      // Do the actual work      result = DoWork.();    }    OnConversionComplete(null, result);    return result;  }  catch(DirectShowException exc)  {    OnConversionComplete(exc, null);    throw;  }  catch(Exception exc)  {    exc = new DirectShowException(exc);    OnConversionComplete(exc, null);    throw exc;  }  catch  {    OnConversionComplete(new DirectShowException(), null);    throw;  }}private DisposalCleanup _dc;protected DisposalCleanup DisposalCleanup { get { return _dc; } }

    DoWork. 方法是抽象方法,如果是 WmvConverter 类,它可以构建筛选器图形并调用 RunGraph 方法。通过这种方式,派生类可以实现 DoWork. 并简单地向基类的 DisposalCleanup 中添加可处置的对象;当派生类的工作执行完毕后,即使它引发异常,基类也会自动处置这些资源。


    --  作者:admin 
    --  发布时间:2005-11-26 3:21:00 
    -- 

    将 WmvConverter 投入使用: WmvTranscoderPlugin

    显而易见,通过前面讨论的代码,您可以编写功能丰富的应用程序来处理 DVR-MS 文件并将其转换成 WMV 文件。但据我所见,此功能最常见的请求是作为 Media Center-集成解决方案的一部分。由此创建了许多非常有用的解决方案,其中最著名的有 Dan Giambalvo 创建的 dCut (可通过 http://www.inseattle.org/~dan/Dcut.htm 下载)以及 Alex Seigler、José Peña、James Edelen 和 Jeff Griffin 创建的 DVR 2 WMV(可通过http://www.thegreenbutton.com/downloads.aspx 下载)。这两个应用程序都依赖于 Alex Siegler 编写的 dvr2wmv DLL(使用的技术与本文所介绍的非常类似,不过采用的是非托管代码)。这些应用程序不懈努力地尝试集成到 Media Center 中,更具体地说是模仿 Media Center 外壳的外观,但遗憾的是,目前的 Media Center SDK 只允许做到这么多。幸运的是,SDK 有另一个相对未开发的区域,它使这种功能可以轻松地集成到 Media Center UI 中,但仍然保留 Media Center 团队已编写的所有烙印:ListMaker 外接程序。

    ListMaker 外接程序是由第三方提供的托管组件,它运行在 Media Center 进程内,使用 Microsoft.MediaCenter.dll 程序集公开的 API 元素(您可以在 Media Center 系统的 %windir%//ehome 目录下找到此 DLL)。ListMaker 外接程序的工作非常简单:它的目的是获取 Media Center 提供给它的文件列表,并对该列表进行一些操作(进行什么操作取决于该外接程序)。Media Center 已将它构建到 UI 中以处理列表生成和随外接程序处理列表时的报告而显示的进程更新。很酷的一点是 Media Center 并不在意该外接程序对媒体列表进行了什么操作。因此,您可以编写这样一个外接程序,它将用户选定的每个 DVR-MS 文件转换成 WMV,并将它们写到硬盘的一个文件夹中。更明确地说,我拥有这样的外接程序(图 9),下面我将向您介绍如何实现。

    图 9. WMV Transcoder 外接程序

    首先,ListMaker 外接程序必须从 System.MarshalByRefObject 派生,如同所有用于 Media Center 的外接程序那样(遗憾的是,SDK 文档目前没有提到这一点,但是这一点非常重要)。Media Center 将所有外接程序加载到一个独立的应用程序域中,这意味着它使用 .NET Remoting 基础结构跨应用程序域边界访问该外接程序。MarshalByRefObject 类能实现这一目的,它允许跨应用程序域边界访问对象,因此外接程序必须以它为基类。如果您忘记从 MarshalByRefObject 派生,则您的外接程序将无法正确加载或运行。

    除了从 MarshalByRefObject 派生外,ListMaker 外接程序还实现了两个来自 Microsoft.MediaCenter.dll 程序集的主要接口:Microsoft.MediaCenter.AddIn.IAddInModule 和Microsoft.MediaCenter.AddIn.ListMaker.ListMaker :

    public class WmvTranscoderPlugin : MarshalByRefObject,   IAddInModule, ListMakerApp, IBrandInfo{  ...}

    所有 Media Center 外接程序都实现了 IAddInModule ,IAddInModule 通过实现 IAddInModule.Initialize 和IAddInModule.Uninitialize 方法来初始化和处置要运行的代码。在许多情况下,初始化阶段需要做的事情非常少;对于我的外接程序,我只需查看一下注册表,找到用户首选项,例如经过编码转换的文件应该写到哪个磁盘(注册表中 HKLM//Software//Toub//WmvTranscoderPlugin 项的 PreferredDrive 值)以及应该使用哪个 Windows Media 配置文件来将代码转换为 WMV(注册表中的 HKLM//Software//Toub//WmvTranscoderPlugin 项的 ProfilePath 值)。如果没有指定驱动器(或者指定的驱动器无效),则我将默认值设置为从System.IO.Directory.GetLogicalDrives 返回的第一个有效的驱动器,其中,有效的驱动器定义为 Win32GetDriveType 函数声明的固定驱动器中的任何一个驱动器。

    ListMakerApp 是列表的主要接口,用于处理和服务双重目的:允许用户选择要处理的媒体文件集(图 10)并启动外接程序的处理,在这之后它允许 Media Center UI 报告进度(图 11)。

    图 10. 选择要进行编码转换的节目

    图 11. Media Center 外壳中的进度更新

    前者涉及的成员并不令人非常满意,所以我不想花太多时间介绍它们。从根本上说,Media Center 通过此接口调用外接程序以获取如选择多少 DVR-MS 文件、还能添加多少文件之类的信息,并在每次用户更改要处理的列表项时调用它。它的核心部分是由三个方法处理的:

    public void ItemAdded(ListMakerItem item){  _itemsUsed++;  _bytesUsed += item.ByteSize;  _timeUsed += item.Duration;}public void ItemRemoved(ListMakerItem item){  _itemsUsed--;  _bytesUsed -= item.ByteSize;  _timeUsed -= item.Duration;}public void RemoveAllItems(){  _itemsUsed = 0;  _bytesUsed = 0;  _timeUsed = TimeSpan.FromSeconds(0);}

    然后通过其他属性和方法(如下所示)公开捕获的信息:

    public TimeSpan TimeUsed { get { return _timeUsed; } }public int ItemUsed { get { return _itemsUsed; } }public long ByteUsed { get { return _bytesUsed; } }public TimeSpan TimeCapacity { get { return TimeSpan.MaxValue; } } public int ItemCapacity { get { return int.MaxValue; } } public long ByteCapacity {   get { return (long)GetFreeSpace(_selectedDrive); } }

    Used 方法只是返回上述方法所维护的计数值。TimeCapacity 和 ItemCapacity 属性同时返回其类型各自的MaxValue 值,因为计算实际用时和实际可用的项数远远超出了本文的讨论范围。ByteCapacity 使用我的私有GetFreeSpace 方法(再次说明,它只是 Win32 GetDiskFreeSpaceEx 函数的一个 p/invoke 包装)来返回磁盘中的可用空间;当然,在与 ByteUsed 配合时这个值也没有什么用处,因为 ByteUsed 表示的是 DVR-MS 文件的大小,而 ByteCapacity 则用于确定磁盘中是否有空间来存放这些文件,但输出文件却是压缩过的 WMV 文件。不过这个实现细节您应该能够自如地进行更改。

    我还将介绍三个更加重要但实现很简单的属性:

    public MediaType SupportedMediaTypes {   get { return MediaType.RecordedTV; } } public bool OrderIsImportant { get { return true; } }public IBrandInfo BrandInfo { get { return this; } }

    SupportedMediaTypes 返回一个加标记的枚举,列出此外接程序支持的媒体类型:可能的类型包括图片、视频、音乐和录制的电视等,Media Center 通常支持所有这些媒体类型。然而,由于此外接程序的主要作用是将 DVR-MS 文件转换成 WMV 文件,因此我将其实现为只从 SupportedMediaTypes 返回MediaType.RecordedTV 。

    Media Center 使用 OrderIsImportant 来确定是否应该允许用户对要处理的录制节目列表重排序。虽然顺序对此外接程序来说并不是真的很重要(因为它只是将文件写到硬盘中),但我想让用户安排某些特定节目在其他节目之前转换(图 12),所以我从这个属性返回 true 而不是 false。

    图 12. 对选定的节目重排序

    BrandInfo 属性允许外接程序的作者修改 Media Center 显示的 UI 以便包含特定于产品的信息。该属性返回一个实现 IBrandInfo 接口的对象。为简单起见,我只在我的外接程序中实现该接口并返回对该外接程序对象自身的引用:

    public class WmvTranscoderPlugin : MarshalByRefObject,   IAddInModule, ListMakerApp, IBrandInfo{  ...  public IBrandInfo BrandInfo { get { return this; } }  ...  public string ViewListPageTitle { get { return "Files to transcode"; } }  public string SaveListButtonTitle { get { return "Transcode"; } }  public string PageTitle { get { return "Transcode to WMV"; } }  public string CreatePageTitle { get { return "Specify target folder"; } }  public string ViewListButtonTitle { get { return "View List"; } }  public string ViewListIcon { get { return null; } }  public string MainIcon { get { return null; } }  public string StatusBarIcon { get { return null; } }  ...}

    IBrandInfo 的八个属性被分成两类:呈现在 UI 中的文本字符串和指定磁盘中图形位置的路径字符串。如果一个属性返回 null,则使用默认值。这样,由于我现在的图形艺术水平还有些欠缺,因此对所有图标属性我都返回 null。这些属性在 UI 中出现的位置如下表所示:


    --  作者:admin 
    --  发布时间:2005-11-26 3:22:00 
    -- 

    属性 
    描述

    PageTitle

    当外接程序使用时显示在右上角的文本。

    CreatePageTitle

    列表创建页面的标题文本。

    SaveListButtonTitle

    用于在列表创建之后启动处理操作的按钮上的文本。

    ViewListButtonTitle

    用于查看要复制以进行处理的媒体项的按钮上的文本。

    ViewListPageTitle

    列表查看页面的标题文本。

    MainIcon

    包含要作为列表生成页面上主图标(水印)使用的图标的文件路径。

    StatusBarIcon

    包含 Media Center 放在生成页面左下角的图标的文件路径。

    ViewListIcon

    Media Center 放在列表查看页面顶部的图标文件的路径。


    --  作者:admin 
    --  发布时间:2005-11-26 3:23:00 
    -- 

    ListMakerApp 上最有趣的方法是 Launch 和 Cancel 。一旦用户创建了要处理的文件列表并单击按钮开始处理,Media Center 就会调用 Launch 方法,它提供三个参数:用户选择的录制节目列表、可被调用以通知 Media Center 状态更新的进程更新委托和应该调用以通知 Media Center 处理完成(成功或因某种异常情况)的完成委托。Launch 方法的作用是立即返回并在后台线程中执行实际的工作。当用户选择取消处理时就会调用 Cancel方法,然后由外接程序停止和终止其操作。

    WmvTranscoderPlugin 的实现遵循这种模式:将 Launch 的参数存储到成员变量中,然后将执行实际转换工作的 ConvertToWmv 方法排入 ThreadPool 队列中:

    public void Launch(ListMakerList lml, ProgressChangedEventHandler pce,   CompletionEventHandler ce){  _listMakerList = lml;  _progressChangedHandler = pce;  _completedHandler = ce;  _cancellationPending = false;  ThreadPool.QueueUserWorkItem(new WaitCallback(ConvertToWmv), null);}private void ConvertToWmv(object ignored){  ThreadPriority oldThreadPriority = Thread.CurrentThread.Priority;  Thread.CurrentThread.Priority = ThreadPriority.Lowest;  try  {    DirectoryInfo outDir = Directory.CreateDirectory(      _selectedDrive + ":" + _listMakerList.ListTitle);    _currentConvertingIndex = 0;    foreach(ListMakerItem item in _listMakerList.Items)    {      if (_cancellationPending) break;      string dvrMsName = item.Filename;      string wmvName = outDir.FullName + "" +         item.Name + ".wmv";      _currentConverter = new WmvConverter(        dvrMsName, wmvName, _profilePath);      _priorCompletedPercentage = _currentConvertingIndex /         (float)_listMakerList.Count;      _currentConverter.PollFrequency = 2000;      _currentConverter.ProgressChanged +=         new ProgressChangedEventHandler(ReportChange);      _currentConverter.Convert();      _currentConverter = null;      _currentConvertingIndex++;    }    _completedHandler(this, new CompletionEventArgs());  }   catch(Exception exc)  {    _completedHandler(this, new CompletionEventArgs(exc));  }  finally  {    Thread.CurrentThread.Priority = oldThreadPriority;  }}

    ConvertToWmv 在选定的驱动器上创建一个目录,使用用户指定的目标文件夹的名称(参见图 13)。然后该方法循环访问所提供的 ListMakerList 中的所有 ListMakerItem 对象,获取 DVR-MS 文件的路径并使用前面构建的 WmvConverter 来将目标目录中的每个 DVR-MS 文件转换成 WMV 文件。Converter 的ProgressChanged 事件关联到外接程序中的一个私有方法 — ReportChange 上,然后由该方法调用 Media Center 的进程更新委托。另外,当前转换程序存储在一个成员变量中,因而可以使用 Cancel 方法来停止其进程


    --  作者:admin 
    --  发布时间:2005-11-26 3:23:00 
    -- 

    Cancel 方法也非常简单。它设置了一个成员变量,用于警告在另一个线程中运行的 ConvertToWmv 方法,通知它用户已经请求取消。然而,正如您在 ConvertToWmv 方法中看到的,只有当该方法准备转换下一个 DVR-MS 文件时才会对此进行检查,所以 Cancel 方法还使用存储在一个成员变量中的 WmvConverter 对象,使用该 Converter 的 CancelAsync 方法取消当前执行的转换。正如我们前面所看到的,这将导致Converter.RunGraph 方法从 WaitForCompletion 方法返回后即刻停止。

    public void Cancel(){  // Cancel any pending conversions  _cancellationPending = true;  // Cancel the current conversion  WmvConverter converter = _currentConverter;  if (converter != null) converter.CancelAsync();}

    我在本文的下载中包含了此外接程序的一个完整的工作实现,包括一个安装程序。该安装程序同时将WmvTranscoderPlugin 的程序集和 WmvConverter 的程序集安装到全局程序集缓存 (GAC) 中,然后使用 RegisterMceApp.exe 工具来将此外接程序通知 Media Center。注册应用程序依赖于一个 XML 配置文件,如下所示:

           

    您应该能够运行安装程序并直接通过一个我们都不必编写的非常时髦的 UI 来将 DVR-MS 立即转换成 WMV。(感谢你,Media Center 团队!)

    图 14. 成功的编码转换

     返回页首

    访问 DVR-MS 元数据

    DVR-MS 文件格式既包含音频、视频和闭合字幕数据,也包含描述文件及其内容的元数据。一旦电视节目录制下来,节目的标题、描述、演员表和原始播放日期等信息就存储在这个位置。很酷的一点是,您的应用程序可以通过 DirectShow StreamBufferRecordingAttribute 对象实现的 IStreamBufferRecordingAttribute 接口轻松地访问此数据。这个对象可以使用它的 CLSID 来创建,正如我本文中创建其他 DirectShow 对象那样。

    要使用 IStreamBufferRecordingAttribute ,首先必须为它提供一个托管接口(您会在本文的代码下载中发现这段代码,它嵌套在 DvrmsMetadataEditor 类中):


    --  作者:admin 
    --  发布时间:2005-11-26 3:24:00 
    -- 

    [ComImport][Guid("16CA4E03-FE69-4705-BD41-5B7DFC0C95F3")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]private interface IStreamBufferRecordingAttribute{  void SetAttribute(    [In] uint ulReserved,     [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName,    [In] MetadataItemType StreamBufferAttributeType,    [In, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute,    [In] ushort cbAttributeLength);  ushort GetAttributeCount([In] uint ulReserved);  void GetAttributeByName(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName,    [In] ref uint pulReserved,    [Out] out MetadataItemType pStreamBufferAttributeType,    [Out, MarshalAs(UnmanagedType.LPArray)] byte[] pbAttribute,    [In, Out] ref ushort pcbLength);  void GetAttributeByIndex (    [In] ushort wIndex,    [In, Out] ref uint pulReserved,    [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszAttributeName,    [In, Out] ref ushort pcchNameLength,    [Out] out MetadataItemType pStreamBufferAttributeType,    [Out, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute,    [In, Out] ref ushort pcbLength);  [return: MarshalAs(UnmanagedType.Interface)]  object EnumAttributes();}

    为了访问 DVR-MS 文件的元数据,我构造了一个 StreamBufferRecordingAttribute 对象并获取它的IFileSourceFilter 接口(您在本文前面也看到了相应的 IFileSinkFilter 接口;它们几乎完全相同)。IFileSourceFilter 的 Load 方法可用于打开我对其元数据感兴趣的 DVR-MS 文件,此时可以获取它的IStreamBufferRecordingAttribute 接口并将该接口用于检索和编辑元数据:

    public class DvrmsMetadataEditor : MetadataEditor{  IStreamBufferRecordingAttribute _editor;  public DvrmsMetadataEditor(string filepath)  {    IFileSourceFilter sourceFilter = (IFileSourceFilter)      ClassId.CoCreateInstance(ClassId.RecordingAttributes);    sourceFilter.Load(filepath, null);    _editor = (IStreamBufferRecordingAttribute)sourceFilter;  }  ...}

    对元数据的读访问是通过 DvrmsMetadataEditor.GetAttributes 方法提供的,该方法提供了IStreamBufferRecordingAttribute 的 GetAttributeCount 和 GetAttributeByIndex 方法的简单抽象。

    public override System.Collections.IDictionary GetAttributes(){  if (_editor == null)     throw new ObjectDisposedException(GetType().Name);  Hashtable propsRetrieved = new Hashtable();  ushort attributeCount = _editor.GetAttributeCount(0);  for(ushort i = 0; i < attributeCount; i++)  {    MetadataItemType attributeType;    StringBuilder attributeName = null;    byte[] attributeValue = null;    ushort attributeNameLength = 0;    ushort attributeValueLength = 0;    uint reserved = 0;    _editor.GetAttributeByIndex(i, ref reserved, attributeName,       ref attributeNameLength, out attributeType,       attributeValue, ref attributeValueLength);    attributeName = new StringBuilder(attributeNameLength);    attributeValue = new byte[attributeValueLength];    _editor.GetAttributeByIndex(i, ref reserved, attributeName,       ref attributeNameLength, out attributeType,       attributeValue, ref attributeValueLength);    if (attributeName != null && attributeName.Length > 0)    {      object val = ParseAttributeValue(        attributeType, attributeValue);      string key = attributeName.ToString().TrimEnd(/'//0/');      propsRetrieved[key] = new MetadataItem(        key, val, attributeType);    }  }  return propsRetrieved;}

    首先,使用 GetAttributeCount 方法来查明要检索的元数据项有多少。然后,对于每个属性,使用GetAttributeByIndex 方法检索属性名的长度和值的长度(以字节为单位)(通过将 name 和 value 参数指定为空值)。当获得长度之后,我就可以创建大小适当的缓冲区来存储数据,并且可以再次调用GetAttributeByIndex 来检索属性的真实名称和字节数组值。如果检索成功,则会根据属性的类型将存储该值的字节数组解析为适当的托管对象。我的 ParseAttributeValue 方法返回 GUID、无符号整型、无符号长整型、无符号短整型、字符串、布尔值或者原始数组(如果值是简单的二进制),这对大多数复杂的元数据属性都是通用的。然后使用该属性的名称及其类型和值构造一个新的 MetadataItem 实例,这个实例将添加到该文件的所有属性的 Hashtable 中。当所有属性都检索完毕时,此集合将返回给用户。

    SetAttributes 方法的工作方式则相反。它是随 MetadataItem 对象集合提供的,其中每个对象都根据其类型格式化为适当的字节数组,然后与 SetAttribute 方法一起使用,以便设置文件的元数据属性:

    public override void SetAttributes(IDictionary propsToSet){  if (_editor == null)     throw new ObjectDisposedException(GetType().Name);  if (propsToSet == null)     throw new ArgumentNullException("propsToSet");  byte [] attributeValueBytes;  foreach(DictionaryEntry entry in propsToSet)  {    MetadataItem item = (MetadataItem)entry.Value;    if (TranslateAttributeToByteArray(      item, out attributeValueBytes))    {      try      {        _editor.SetAttribute(0, item.Name,           item.Type, attributeValueBytes,           (ushort)attributeValueBytes.Length);      }      catch(ArgumentException){}      catch(COMException){}    }  }}

    MetadataItem 是一个属性的名称、值和类型的简单包装。MetadataItemType 是有效类型(GUID、字符串、无符号整型等)的枚举。


    --  作者:admin 
    --  发布时间:2005-11-26 3:24:00 
    -- 

    您可能注意到 DvrmsMetadataEditor 类是从 MetadataEditor 基类派生的。我这样做是为了提供另一个类 —AsfMetadataEditor ,它也是从 MetadataEditor 派生的。AsfMetadataEditor 基于包含在 Windows Media Format SDK(从此处下载 SDK )中的示例代码。它使用 Windows Media IWMMetadataEditor 和IWMHeaderInfo3 接口来获取 WMA 和 WMV 文件(这两者都基于 ASF 文件格式)的相关元数据信息。您可能会发现,当前这些 Windows Media Format SDK 接口除了能用于处理 WMA 和 WMV 文件外,还可以处理 DVR-MS 文件,不过将来可能不再这样,而且 Microsoft 强烈建议使用 IStreamBufferRecordingAttribute 接口来处理 DVR-MS 文件。IWMHeaderInfo3 接口的相关部分与 IStreamBufferRecordingAttribute 接口几乎相同,因此 AsfMetadataEditor 类和 DvrmsMetadataEditor 类也极其相似。

    在这些类就位后,将元数据从一个媒体文件复制到另一个(例如从 DVR-MS 文件复制到经过代码转换的 WMV 文件)就变得极为简单,从而让您保持与经过编码转换的 TV 录制相关联的元数据的保真度:

    using(MetadataEditor sourceEditor = new DvrmsMetadataEditor(srcPath)){  using(MetadataEditor destEditor = new AsfMetadataEditor(dstPath))  {    destEditor.SetAttributes(sourceEditor.GetAttributes());  }}

    实际上,正是出于从一个媒体文件向另一个媒体文件复制元数据的目的,我在 MetadataEditor 类中创建了一个静态的 MigrateMetdata 方法,这个方法不仅能按上述方式迁移元数据,而且对它加以扩大,这样在 Media Player 中查看 DVR-MS 文件和在 Media Center 中播放 WMV 文件时,就可以显示更多的可用信息。

     返回页首

    编辑 DVR-MS 文件

    除了转换为 WMV 之外,编辑和拼接 DVR-MS 文件可能是我在网上新闻组中看到的第二个最常请求的功能。许多人没有意识到的是,DirectShow RecComp 对象及其 IStreamBufferRecComp 接口提供了现成的拼接功能。IStreamBufferRecComp 接口用于从现有的录制片段创建新的录制,以及将来自一个或多个 DVR-MS 文件的片段连接在一起。

    IStreamBufferRecComp 接口非常简单,它的一个 C# 导入如下所示:

    [ComImport][Guid("9E259A9B-8815-42ae-B09F-221970B154FD")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IStreamBufferRecComp{  void Initialize(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszTargetFilename,     [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecProfileRef);  void Append(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording);  void AppendEx(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording,    [In] ulong rtStart, [In] ulong rtStop);  uint GetCurrentLength();  void Close();  void Cancel();}

    要拼接 DVR-MS 文件,首先要创建 RecComp 对象的实例。这可以通过本文前面介绍的ClassId.CoCreateInstance 方法来完成,代码如下:

    IStreamBufferRecComp recCom =   (IStreamBufferRecComp)ClassId.CoCreateInstance(ClassId.RecComp)and with ClassId.RecComp defined aspublic static readonly Guid RecComp =   new Guid("D682C4BA-A90A-42FE-B9E1-03109849C423");

    有了 IStreamBufferRecComp 之后,就可以使用它的 Initialize 方法来为新的录制指定输出文件名。另外,Initialize 的第二个参数应该是要拼接的其中一个 DVR-MS 输入文件的文件路径。IStreamBufferRecComp支持连接来自一个或多个文件的片段,但所有这些文件必须使用相同的配置文件录制,这意味着它们必须使用 Media Center 中的相同配置和设置进行录制。RecComp 需要知道输出文件使用什么配置文件,因此您必须指定一个输入文件作为第二个参数,以便它可以检查其配置文件信息并将该信息作为输出文件的基础。

    一旦初始化了 IStreamBufferRecComp ,您就可以开始构建新文件。调用 Append 方法,指定一个 DVR-MS 输入文件的完整路径,则整个文件就会追加到输出文件中。AppendEx 方法允许您指定附加的开始和停止时间,以便只使用输入文件的一部分并将这部分追加到输出文件中。在非托管接口中,这些时间被定义为REFERENCE_TIME — 一个代表以 100 毫微秒为单位的数值的 64 位长整数值,所以在托管代码中,您可以使用如下所示的函数来将秒转换成传递给 AppendEx 的 REFERENCE_TIME 值:

    internal static ulong SecondsToHundredNanoseconds(double seconds){  return (ulong)(seconds * 10000000);}

    当您完成追加到输出文件时,Close 方法就会关闭输出文件。在您连接到该文件时,可以使用一个单独线程的GetCurrentLength 方法来确定输出文件的当前长度。然后您可以使用此信息和您对输入文件/片段长度的了解来计算完成拼接的百分比。请注意,这个过程非常快,因为将片段从一个 DVR-MS 文件追加到另一个文件并不需要编码和解码。


    --  作者:admin 
    --  发布时间:2005-11-26 3:25:00 
    -- 

    为了演示此接口,我构建了 DVR-MS 编辑器应用程序(如图 15 所示),并将它作为与本文有关的代码下载的一部分。

    图 15. DVR-MS 编辑器

    这个应用程序其实非常简单,用了一个多小时就实现了。它使用 Windows Media Player ActiveX 控件来显示输入的视频文件。为了加载视频文件,它将 AxWindowsMediaPlayer.URL 属性设置为 DVR-MS 文件的路径,这样可以使 Media Player 加载该视频(如果 AxWindowsMediaPlayer.settings.autoStart 属性为真,它还会开始播放)。

    一旦加载了视频,用户就可以使用“Media Player”工具栏对它进行控制,这个工具栏可以使用户完全控制视频的播放和搜索。当到达用户想要开始或停止一段视频的位置时,就会查询AxWindowsMediaPlayer.Ctlcontrols.currentPosition 属性。然后,刚才描述的 IStreamBufferRecComp接口可以使用这些时间来创建输出文件。

    另外,Media Player 对视频的当前位置提供了细粒度的编程控制。您可以使用如下所示的代码来逐帧移动视频:

    ((WMPLib.IWMPControls2)player.Ctlcontrols).step(1);

    或者,还可以通过设置刚才讨论的 AxWindowsMediaPlayer.Ctlcontrols.currentPosition 来跳转到视频中的特定位置。

    DVR-MS 编辑器应用程序还利用了本文前面描述的一些其他技术,例如将元数据从源视频文件复制到输出视频文件。

     返回页首

    小结

    这是令人惊讶的技术,不是吗?DirectShow 和 Windows XP Media Center Edition 团队为开发人员提供了许多处理 DVR-MS 文件的工具(包括非托管代码的和托管代码的)。通过使用这些工具,可以创建新的应用程序来提供大多数人没有意识到他们能够使用的真正强大的功能。本文所讨论的主题只涉及到您可以用来处理 DVR-MS 文件的各种技术的一部分,而在人们编写的使用这些库和工具的解决方案中,它们所占的比例则甚至更小。我期待着获悉您使用这种功能来开发解决方案。

    现在,我要回去看会电视了。


    相关书籍

    Programming Microsoft DirectShow for Digital Video and Television (Microsoft Press, 2003)

    Fundamentals of Audio and Video Programming for Games (Microsoft Press, 2003)


    展开全文
  • DirectShow简介

    千次阅读 2017-12-11 18:07:25
    3.1 DirectShow 的功用 (1) 保证数据量庞大的多媒体数据处理的高效性; (2) 时刻保持音频和视频的同步; (3) 用简单的方法处理复杂的媒体源问题,包括本地文件,计算机网络,广播电 视以及其他数码产品; (4) 处理...
    3.1 DirectShow 的功用
    (1) 保证数据量庞大的多媒体数据处理的高效性; (2) 时刻保持音频和视频的同步; (3) 用简单的方法处理复杂的媒体源问题,包括本地文件,计算机网络,广播电 视以及其他数码产品; (4) 处理各种各样的媒体格式问题,包括 AVI,ASF,MPEG,DV,MOV 等; (5) 支持目标系统中不可预知的硬件; DirectShow 的设计初衷就是尽量要让应用程序开发人员从复杂的数据传输, 硬件差异, 同步性等工作中解放出来, 总体应用框架和底层工作由 DirectShow 来完成。

    图 3.1 DirectShow 系统 Ring0 特权级别的硬件设备,Ring3 特权级别的应用层。 按照功能 Filter 分为三类:Source Filters-主要负责获取数据,数据源可以是 文件,因特网计算机里的采集卡 (WDM 驱动或 VFM 驱动 ) 数字摄像机等; Transform Filters-主要负责数据的格式转换, 例如数据流分离/合成, 解码/编码等; Rendering Filters-主要负责数据的最终去向将数据传给显卡,声卡进行多媒体的 演示,或者输出到文件进行存储。

    3.2 COM 编程基础
    如何创建 COM 组件 如何得到组件对象上的接口以及调用接口方法 如何管理组件对象(COM 的引用记数机制)
    —DirectShow 应用程序开发人员

    COM 本身是一种规范,而不是实现 COM 组件是一个 C++类,其接口都是纯虚类
    —Filter 开发人员

    Class Ifuction //接口 { public: virtual Method1()=0; virtual Method2()=0; }; Class MyObject:public Ifunction //COM 组件 { public: virtual Method1(); virtual Method2(); }; CoInitialize(NULL); //COM 库初始化 //Do something IUnkown *pUnk=NULL;//COM 规范规定,任何组件或接口都必须从 IUnkown 接 口中继承 IObject *pObject=NULL; //创建组件对象 HRESULThr=CoCreateInstance(CLISD_Object, CLSCTX_INPROC_SERVER,NULL,IID_IUnkown,(void**)&pUnk); if(SUCCEEDED(hr)) { //查询得到组件对象上的接口 Hr=pUnk->QueryInterface(IID_IObject,(void**)&pObject; //IUnkown 中负责组件对象上的接口查询的虚函数 If(SUCCEEDED(hr)) { //调用接口方法 pObject->SomeMethod(); //IUnkown 中 AddRef 用于增加引用计数 pObject->Release(); //IUnkown 中 Release 用于减少引用计数 } pUnk->Release; } CoUninitialize();//释放 COM 库使用资源 引用计数是 COM 中一个非常重要的概念,它很好的解决了组件对象的生命 周期问题,即 COM 组件到底在什么时候被销毁,以及由谁来销毁的问题。 COM 组件有三种类型:进程内组件,本地进程组件和远程组件。Filter 一般 是一种进程内组件,以 DLL(动态链接库)的形式提供服务。 COM 规 范 规 定 , 每 个 组 件 都 必 须 实 现 一 个 与 之 相 对 应 的 类 工 厂

    (ClassFactory). 类工厂也是一个 COM 组件,用 IClassFactory 接口来实现,在 IClassFactory 的接口函数 CreateInstance 中, 才能使用 new 操作生成一个 COM 组件类
    对象实例。

    CoCreateInstance() { IClassFactory *pClassFactory=NULL; CoGetClassObject(CLSID_Object,CLSCTX_INPROC_SERVER,NULL,IID_ICl assFactory,(void**)&pClassFactory); //通过每个 COM 组件唯一标识调用 CoGetClassObject 来获得创建这个组件对 象的类工厂 pClassFactory->CreateInstance(NULL,IID_IUnkown,(void**)&pUnk); //调用类工厂接口方法创建 CLSID_Object 标识的真正组件 pClassFactory->Release(); } CoGetClassObject() { //通过查询注册表 CLSID_Object 得知组件 DLL 文件路径 //装入 DLL 库(调用 LoadLibrary) //使用函数 GetProcAddress()得到 DLL 中函数 DllGetClassObject 的函数指针 //调用 DllGetClassObject 得到类工厂对象指针 } DllGetClassObject() //必须实现一个(导出)函数,根据指定的组件 GUID 创建相应的类工厂对象 //返回类工厂 IClassFactory 接口 {

    //创建类工厂对象 CFactory* pFactory= new CFactory; //查询得到 IClassFactory 指针 pFactory->QueryInterface(IID_IClassFactory,(void**)&pClassFactory); pFactory->Release(); } CFactory::CreateInstance()//IClassFactory 接口的方法,负责最终创建组件对象实 例 { //创建 CLSID_Object 对应的组件对象 CObject *pObject= new CObject; //我们的 COM 组件类,实现 COM 框架以外的真正组件功能 pObject->QueryInterface(IID_IUnkown,(void**)&pUnk); pObject->Release(); } 典型自注册 COM 组件 DLL 所必需的 5 个(导出)函数: DllMain:DLL 的入口函数(DirectShow 实现的是 DllEntryPoint); DllGetClassObject:在创建 Filter 对象时被调用, 根据 CLSID 返回对应类工厂指针; DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载 DLL; DllRegisterServer:将 COM 组件注册到注册表中; STDAPI DllRegisterServer() { return AMovieDllRegisterServer2(TRUE); } DllUnregisterServer:删除注册表中 COM 组件的注册信息; STDAPI DllUnregisterSever() { return AMovieDllREgisterServer2(FALSE); }

    第四章 Filter 原理
    4.1 Filter 概述
    Filter 一般由一个或多个 Pin 组成,Filter 之间通过 Pin 相互连接,构成一条顺序 链路。Filter 是一种 COM 组件,为实现在 FilterGraph 中的统一操作,每个 Filter 上都至少实现了 IBaseFilter 接口,实现 Filter 的文件一般是一个 DLL,扩展名可 以是 dll,但更多的时候是 ax.。 跟普通的 COM 组件一样, Filter 的创建是通过 API 函数 CoCreateInstance 来完成的: STDAPI CoCreateInstance{ REFCLSID rclsid,//指定要创建的 Filter 的 CLSID LPUNKOWN pUnkOutter, //绝大多数情况下创建的 Filter 不是被“聚合“的,所以 pUnkOuter 指定为 NULL DWORD dwClsContext,//可指定为 CLSCTX_INPROC_SERVER, 以创建进程内组 件对象

    REFIID riid, //riid 在创建 Filter 成功后获得的接口的 ID,一般为 IID_IBaseFilter,也可以是其他 特殊接口 LPVOID *ppv//用于获得接口对象的指针 }

    4.2 Filter 的注册
    Filter 的注册程序为 regsvr32.exe (位于操作系统目录的 system32 子目录下) 。 假设现在有一个 Filter 文件,它的完整路径为 C:\DSFilter\myFilter.ax. 注册这个 Filter 的方法为:在命令行的状态下,执行 regsvr32 C:\DSFilter\myFilter.ax.随后 会弹出一个对话框,告诉 Filter 是否注册成功。注销 Fliter 也使用 regsvr32.exe 程序,方法为加命令行参数/u,即执行 regsvr32/u C:\DSFilters\myFilter.ax. 判断 Filter 是否注册 BOOL IsFilterRegistered(CLSID inFilterId) { IBaseFilter *pFilter =NULL; If(SUCCEEDED(CoCreateInstance(inFilterId,NULL,CLSCTX_INPRO_SERVER, IID_IBaseFilter,(void)**&pFilter))) {pFilter->Release(); return TURE;} Return FALSE; } 在应用程序中注册(或注销)某个 Filter 文件 只要在应用程序中使用 LoadLibrary 装载这个 Fliter 文件,并得到它的导出 函数 DllRegisterServer(DllUnregisterSever)的入口地址,然后执行。 BOOL RegisterFilter(const char *inFilterAx) { typedef (WINAPI *REGISTER_FUNC) (void); REGISTER_FUNC MyFunc= NULL; HMODULE hModule =::LoadLibrary(inFilterAx); If(hModule) { MyFunc=(REGISTER_FUNC)GetProAddress(hModule,”DllRegisterServer”); BOOL pass=(MyFunc!=NULL); If(pass) { MyFunc(); } ::FreeLibrary(hModule); return pass; } return FLASE;

    } Filter 的注册信息一般包括两部分:基本的 COM 信息和 Filter 特有信息。前 者是基本的 COM 组件所必需的信息;后者是描述 Filter 的信息(包括 Filter 注 册的类型目录,Filter 上的 Pin 数量,支持的媒体类型等) ,这部分内容会被系 统枚举器或者 Filter 影射器访问到,但不是必须的。

    4.3 Filter 的媒体类型
    类型媒体用来描述格式化的数据流, DirectShow 中定义一个数据结构 AM_MEDIA_TYPE: typedef struct _MediaType{ GUID majortype;//主类型 GUID subtype;//辅助说明类型 BOOL bFixedSizeSample;// BOOL bTemporalcompression; ULONG ISampleSize; GUID formattype;//格式细节类型 IUnkown *pUnk; ULONG cbFormat; /*[size_is]*/ BYTE *pbFormat; }AM_MEIDA_TYPE; 当 使 用 AM_MEDIA_TYPE 数 据 结 构 描 述 媒 体 类 型 时 , 如 果 majortype,subtype 和 formatype 都指定一个特定的 GUID 值,称为“完全指定媒 体类型” ;这三个部分只要有一个指定为 GUID_NULL,则称之为“不完全指定 的媒体类型” 。GUID_NULL 具有“通配符”的作用。

    4.4 Filter 的连接
    Filter 的 连 接 世 上 是 Fliter 上 Pin 的 连 接 , 连 接 的 方 向 总 是 由 上 一 级 Filter(Upstream Filter)的输出 Pin 指向下一级 Filter(Downstream Filter)的输入 Pin。 Pin 的连接实际上是连接双方使用的媒体类型的一个“协商”过程。

    4.4.1 连接过程
    Pin 也是一种 COM 组件, 而且每个 Pin 上都实现了 IPin 接口。 首先连接 Filter 的 是 应 用 程 序 , 一 般 通 过 调 用 接 口 方 法 IFilterGraph::ConnectDirect, IGraphBuilder::Connect, IGraphBuilder::Render 或者 IGraphBuilder::RenderFile 来 实现。 试图连接的两个 Filter 必须处在同一 Filter Graph 中,调用接口方法 IFilterGraph::AddFilter 将指定的 Filter 加入到 FilterGraph 中。对于 Filter 的加入 或 者 移 走 , Filter Graph Manager 会 调 用 该 Filter 上 的 接 口 方 法

    IBase-Filter::JoinFilterGraph 通 知 。 对 于 Filter 开 发 人 员 来 说 , 可 以 重 写 CBaseFilter::Join-FilterGraph 函数来捕捉此事件。

    整个连接过程的大致步骤: (1) Filter Graph Manager 在输出 Pin 上调用 IPin::Connect(带输入 Pin 的指针 作为参数) ; (2) 如果输出 Pin 接受连接,则调用输入 Pin 上的 IPin::ReceiveConnection; (3) 如果输入 Pin 也接受这次连接,则双方连接成功; STDMETHODIMP CBasePin::Connect(IPin* pReceivePin, Const AM_MEDIA_TYPE *pmt //optional media type) { CheckPointer(pReceivePin,E_POINTER);//指针是否有效 ValidateReadPtr(pReceivePin,sizeof(IPin));//分配内存是否成功 CAutoLock cObjectLock(m_pLock);//CCirSec Class 临界 DisplayPinInfo(pReceivePin); //检查该 Pin 是否早已连接 if(m_Connected){ DbgLog((LOG_TRACE,CONNECT_TRACE_LEVEL,TEXT(“Already connected”))); return VFW_E_ALREDY_CONNECTED; } //一般 Filter 只能在停止状态下进行连接 If(!IsStopped()&&!m_bCanReconnectWhenActive){ return VFW_E_NOT_STOPPED;} //开始媒体类型的检查过程,找出一种连接双方都支持的媒体类型 const CMediaType *pType=(CMediaType*)pmt; HRESUSLT hr=AgreeMeidaType(pReceivePin,pType);//pType 必 须 是 const

    if(FAILED(hr)){ DbgLog((LOG_TRACE,CONNECT_TRACE_LEVEL,TEXT(“Failed to agree type”))); EXECUTE_ASSERT(SUCCEEDED(BreakConnect())); return hr;} DbgLog((LOG_TRACE,CONNECT_TRACE_LEVEL,TEXT(“Connection succeeded”))); return NOERROR; } 上述函数没有真正实现 Pin 连接过程, 只是进行了参数的检查以及状态的检 查,这个函数是从输出 Pin 进入的;对于真正的连接过程,还要进一步查看 AgreeMediaType 函数: HRESULT CBasePin::AgreeMediaType(IPin *pReceivePin,const CMediaType *pmt) { ASSERT(pReceivPin);//诊断函数,如返回失败,输出宏信息 IEnumMediaTypes *pEnumMediaTypes=NULL; //判断 pmt 是不是一个完全指定的媒体类型 if((pmt!=NULL)&&(!pmt->IsPartiallySpecified())){ //用这个完全指定的媒体类型进行测试连接, 如果连接失败, 不再作其他 尝试 return AttemptConnection(pReceivePin,pmt); } HRESULT hrFailure = VFW_E_NO_ACCEPTABLE_TYPES; //进行 Pin 上支持的媒体类型枚举,开始媒体类型的“协商”过程 for(int i=0;i<2;i++) { HRESULT hr; if(i= =(int)m_bTryMyTypesFirst){//输出 Pin 上的成员变量, 初始值为 false hr=pReceivePin->EnumMediaTypes(&pEnumMediaTypes); }eslse{ hr=EnumMediaTypes(&pEnumMediaTypes);}//输出 Pin 上的枚举器 if(SUCCEEDED(hr)){ ASSERT(pEumMediaTypes); hr=TryMediaTypes(pReceivePin,pmt,pEnumMediaTypes); pEnumMediaTypes->Release(); if(SUCCEEDED(hr)){ return NOEEROR; }else{ If((hr!=E_FAIL)&& (hr!=E_INVALIDARG) && (hr!=E_VAW_E_TYPE_NOT_ACCEPTED) ){ hrFailure=hr;} }

    } } return hrFailure; } 如果 pmt 是一个空指针,或者 pmt 包含的是一个不完全指定的媒体类型, 那么真正的协商过程开始了。注意,for 循环的循环次数为 2:输出 Pin 上的成 员变量 m_bTryMyTypesFires 初始值为 FLASE。 也就是说, 连接进程进行到这里, 会首先得到输入 Pin 上的媒体类型枚举器的试连接;如果不成功,再得到输出 Pin 上的媒体类型枚举器的试连接。 HRESULT CBasePin::TryMediaTypes(IPin *pReceivePin,const CMediaType *pmt, IEnumMediaTypes *pEnum) { //复位枚举器内部状态 HRESULT hr=pEnum->Reset(); if(FAILED(hr)){ return hr;} CMediaType *pMediaType=NULL; ULONG ulMediaCount=0; HRESULT hrFaillure =S_OK; for(;;){ hr=pEnum->Next(1,(AM_MEDIA_TYPE**)&pMediaType,&ulMediaCount); //枚举 Pin 上所提供的所有媒体类型 if(hr!=S_OK){ (S_OK==hrFailure){ hrFailure=VFW_E_NO_ACCEPTABLE_TYPE;} return hrFailure;} ASSERT(ulMediaCount= =1); ASSERT(pMediaType); //检查当前枚举得到的媒体类型是否与不完全指定的媒体类型参数匹配 if((pmt==NULL)||pMediaType->MatchesPartial(pmt)){// 是否跟不完全类型 匹配 //进行试连接 hr=AttempConnection(pReceivePin, pMediaType);//是否跟输入 Pin 媒体匹 配 if(FAILED(hr)&& SUCCEEDED(hrFailure)&& (hr!=E_FAIL) && (hr!=E_INVALIDARG) && (hr!=E_VAW_E_TYPE_NOT_ACCEPTED)){ hrFailure=hr;} else{ hr=VFW_E_TYPE_NO_ACCEPTABLE_TYPES;} if(S_OK==hr){

    return hr;} } } 当连接进程进入 TryMediaTypes 函数后,会使用媒体类型枚举器枚举 Pin 上 提供的所有媒体类型,然后一种一种的进行试连接(调用 AttemptConnection 函 数) 。在枚举过程中,如果有一种媒体类型试连接成功,那么整个 Pin 连接过程 也就宣告成功。如果媒体类型都未获成功,TryMediaTypes 函数会返回一个错误 的值。如果使用输入 Pin 和输出 Pin 的媒体类型枚举器进行连接都失败了,那么 本次 Pin 连接宣告彻底失败。 最后需要看看 AttempConnection 函数中所做的工作。对于 Filter 开发人员, 这部分内容显得尤为重要。因为可以从 AttemptConnection 函数的实现中,清楚 的看到一些基类跟 Pin 连接相关的虚函数的调用顺序, 这对以后自己写 Filter 时, 选择需要重写的基类虚函数具有指导意义。 HRESULT CBasePin::AttemptConnection{IPin *pReceivePin,const CMediaType* pmt} { //获取 Filter 对象上的操作权 ASSERT(CritCheckIn(m_pLock)); HRESULT hr=CheckConnect(pReceivePin); if(FAILED(hr)){ DbgLog((LOG_TRACE,CONNECT_TRACE_LEVEL,TEXT(“CheckConnect faliled”))); EXECUTE_ASSERT(SUCCEEDED(BreakConnect())); return hr;} //一个很有用的调试函数,可以显示媒体类型 DisplayTypeInfo(pReceivePin,pmt); //Pin 上的媒体类型检查 hr=CheckMediaType(pmt); if(hr==NOERROR){ m_Connected=pReceivePin;//输出 Pin 上的成员变量 m_Connected->AddRef(); hr=SetMediaType(pmt);//在输出 Pin 上保存媒体类型 if(SUCCEEDED(hr)){ //询问连接对方 Pin 是否也能接受当前的媒体类型 hr=pReceivePin->ReceiveConnection((IPin)this,pmt); if(SUCCEEDED(hr){ //连接完成 Hr=CompleteConnect(pReceivePin); if(SUCCEEDED(hr)){ return hr;} else{ DbgLog((LOG_TRACE,CONNECT_TRACE_LEVEL,TEXT(“Failed to complete connection”))) ;}}}}

    else{ if(SUCCEEDED(hr)||(hr==E_FAIL)||(hr==E_INVALIDAGR)){ hr=VFW_E_TYPE_NOT_ACCEPTED;}} EXECUTE_ASSERT(SUCCEEDED(BreakConnect())); /*If failed then undo our state*/ if(m_Connected){ m_Connected->Release(); m_Connected=NULL;} return hr; } 在 AttemptConnection 函数实现中,首先调用输出 Pin 上的 CheckConnect 函 数进行连接检查,例如查看接受连接的 Pin 是否支持某个特殊的接口等。如果 CheckConnect 失败,会调用 BreakConnect 。 如果成功,会继续调用输出 Pin 上 的 CheckMediaType,进行连接用的媒体类型检查。如果 CheckMediaType 也成 功,则会将输入 Pin 对象指针保存在输出 Pin 对象中,然后调用输入 Pin 上的 ReceiveConnection 函数。该函数在输入 Pin 上也进行了一系列检查,检查内容以 及次序与上述输出 Pin 上的类似。如果 ReceiveConnection 调用成功,最后就调 用输出 Pin 上的 CompleteConnect 函数。 输出 Pin 上的 CompleteConnect 函数中完成收尾工作如:用于数据传送的内 存怎么分配,谁来管理这些内存,然后才能在 Pin 之间开始传输数据: HRESULT CBaseOutputBin::CompleteConnect(IPin *pReceivePin) { UNREFERENCED_PARAMETER(pReceivePin); return DecideAllocator(m_pInputBin,&m_pAllocator);//完成内存分配器协商} 在 DirectShow 中,数据传送单元叫做 Sample(COM 组件,管理一块数据内 存) ;而 Sample 是由分配器 Allocator(COM 组件)来管理。连接双方的 Pin 必 须使用同一个分配器但是这个分配器到底由哪个 Pin 来建立也需要协商。 HRESULT CBaseOutputPin::DecideAllocator(IMemInputPin* pPin,IMemAllocator **ppAlloc) { HRESULT hr=NERROR; *ppAlloc=NULL; ALLCATOR_PROPERTIES prop; ZeroMemory(&prop,sizeof(prop)); //询问输入 Pin 对分配器的要求 pPin->GetAllocatorRequirements(&prop); if(prop.cbAlign= =0){ prop.cbAlign=1;} // 询问输入 Pin 是否提供一个分配器 hr=pPin->GetAllocator(ppAlloc); if(SUCCEEDED(hr)) { //决定 Sample 使用的内存大小,以及分配器管理的 Sample 数量 hr=DecideBufferSize(**ppAlloc,&prop);

    if(SUCCEEDED(hr)){ //通知输入 Pin 最终使用的分配器对象 hr=pPin->NotifyAllocator(*ppAlloc,FALSE); if(SUCCEEDED(hr)){ return NOERROR; }}} //如果输入 Pin 上不提供分配器,则必须在输出 Pin 上创建一个分配器 if(*ppAlloc){ (*ppAlloc)->Release(); *ppAlloc=NULL;} // 创建一个输出 Pin 上的分配器 hr= InitAllocator(ppAlloc); if(SUCCEEDED(hr)){ hr=DecideBufferSize(*ppAlloc,&prop); if(SUCCEEDED(hr)){ hr=pPin->NotifyAllocator(*ppAlloc,FALSE); if(SUCCEEDED(hr)){ return NOERROR;}}} if(*ppAlloc){ (*ppAlloc)->Release(); *ppAlloc=NULL;} return hr; } 当 Pin 上的数据传送内存分配协商成功后,实际上并没有马上分配 Sample 的内存。实际内存的分配,一般是在 FilterGraph 运行之后,调用输出 Pin 上的 Active 函数时进行的,代码如下: HRESULT CBaseOutputPin::Active(void){ if(m_pAllocator==NULL){ return VFW_E_NO_ALLOCATOR;} return m_pAllcator->Commit(); }

    4.4.2 智能连接
    FilterGraph 的构建方法: IFilterGraph::AddFilter:该参数提供一个 Filter 对象,将其加入到 FilterGraph 中 IFilterGraph::ConnectDirect:该参数提供输出 Pin,输入 Pin 以及媒体类型,进行 直接的连接 IGraphBuilder::AddSourceFilter::该参数提供源文件名,自动将一个 SourceFilter 加载到 FilterGraph 中 IGraphBuilder::Connect:该参数提供输出 Pin 和输入 Pin 进行连接,如果连接失 败,自动尝试在中间插入必要的格式转换 Filter IGraphBuilder::Render:该参数提供输出 Pin,自动加入必要的 Filter 完成剩下部 分 Filter Graph 的构建(直到连接到 Rendering Filter) IGraphBuilder::RenderFile:该参数提供源文件名,自动加入必要的 Filter 完成这

    个文件的回放 FilterGraph 构建 我们可以看到后四种构建方法都有“自动”功能,在 DirectShow 中,称为 智能连接。在介绍智能连接的具体实现过程前,先要来看一下 Filter 的 Merit 值。 这个 Merit 值可以理解成为 Filter 被智能连接错使用的优先级。DirectShow 定义 如下六种标准的 Merit 值: Enum { MERIT_PREFERED = 0x800000, MERIT_NORMAL =0x600000, MERIT_UNLIKELY= 0x400000, MERIT_DO_NOT_USE =0x200000, MERIT_SW_COMPRESSOR =0x100000, MERIT_HW_COMPRESSOR =0x100050}; Filter 的 Merit 值只有在大于 MERIT_DO_NOT_USE 的时候,才有可能被智 能连接所使用,而在等同条件下(几个 Filter 完成的功能相同) ,Merit 的值越大, 这个 Filter 被使用的机会就越大。 打开注册表,在 Filter 信息的注册部分都会发现一个称为 FilterDate 的二进 制串类型的键值,这个键值记录的内容就包含 Merit 信息,可以如下定义一个数 据结构来操作 FilterData 值开始部分的一块数据: Typedef struct { DWORD dwVersion;//版本号 DWORD dwMerit;//Merit 值 DWORD dwPinCount;//Pin 的数量 DWORD dwReserved;//保留 }FILTER_HEADER; BOOL SetFilterMerit(const char* inClsid,DWORD inMerit) { const char* cRegistryEntry =”CLSID\\{083863f1-70DE-11d0-BD4000A0C911CE86}”}\\Instance\\”; const long cMaxLength=1024*16; BYTE filterData[cMaxLength]; DWORD actualLength=0; //生成 Filter 信息注册部分的注册表入口 char szEntry[1000]; strcpy(szEntry,cRegistryEntry);//前面是目的文件,后面是源文件 strcat(szEntry,inClsid);//追加写 HKEY hKey=NULL; LONG result =::RegOpenKeyEx(HKEY_CLASSES_ROOT,szEntry,0, KEY_ALL_ACCESS,&hKey); BOOL pass=(result==ERROR_SUCCESS); if(pass) { //读取 FilterData 的值

    actualLength =actualLength; result=::RegQueryVallueEx(hKey,”FilterData”,NULL, NULL,filterData,&actualLength); pass =(result ==ERROR_SUCCES); } if(pass) { //修改 FilterData 中的 Merit 部分,然后写回注册表 FILTER_HEADER *filterHeader=(FILTER_HEADER) filterData; filterHeader->dwMerit = inMerit; result = ::RegSetValueEx(hKey,”FilterData”,NULL, REG_BINARY,filterData,actualLength); Pass= (result = =ERROR_SUCCESS); } if(hKey) { ::RegCloseKey(hKey);} return pass;} IGraphBuilder::AddSourceFilter( LPCWSTR lpwstrFileName, LPCWSTR lpwstrFilterName,IbaseFilter **ppFilter 该方法给出一个源文件路径,FilterGraphManager 能够为这个源文件加载一 个相应的 SourceFilter。下面是分析文件路径以及查询注册表的过程。 首先分析文件路径中是否有协议名 (如 http,htp 等, FilterGraphManager 认为 冒号前的字符串为协议名, 如 myprotocol://myfile.ext 中, myprotocol 就是协议名) 。 如果有则在注册表的如下位置寻找这个协议名。 HKEY_CLASSES_ROOT <protocol> Source Filter =<Source filter CLSID> Extensions <.ext1>=<Source filter CLSID> <.ext2>=<Source filter CLSID> 找到后,先在 Extensions 子键下匹配扩展名,如果扩展名匹配不成功,则使 用 SourceFilter 键值指定的 CLSID,默认情况下 (注册表中找不到文件路径中的协 议名) ,FilterGraphManager 创建一个称为 FilterSource(URL)的 Filter. 如果文件路径中不包含协议名,FilterGraphManager 就分析这个文件的扩展 名, 然后到注册表的 HKEY_CLASSES_ROOT\Media Type\Extensions 子键下去匹 配扩展名。如果找到了,就根据 Source Filter 键值指定的 CLSID 创建 Filter;找不 到,FilterGraphManager 最后只能进行文件的校验字节判断。 HRESULT IGraphBuilder::RenderFile(LPCWSTR lpwstrFile,LPCWSTR lpwstrPlayList) 该方法给出一个文件名,首先根据与 IGraphBuilder::AddSourceFilter 类似的 算法找到创建正确的 SourceFilter。然后,就从该 SourceFilter 的各个输出 Pin 开 始,进行剩下的智能连接过程。这是一个“递归”过程,直到所有的分支都连接 到一个 Rendering Filter 上为止。

    如 果 输 出 Pin 支 持 IStreamBuilder 接 口 , 则 把 剩 下 的 工 作 交 给 IStreamBuilder::Render; 使用在 FilterGraphManager 内部缓冲中的 Filter 进行试连接; 使用在当前 FilterGraph 中还没有完全连接的 Filter 进行试连接。使用 IFilterMapper2::EnumMatchingFilters 搜索注册表。FilterGraphManager 使用 Merit 值大于 MERIT_DO_NOT_USE 的所有 Filter (Filter 所在的类型目录的 Merit 值也 应该大于 MERIT_DO_NOT_USE)进行试连接。在匹配 Mediatype 的前提下, Merit 值越高,该 Filter 被使用的概率就越高。 HRESULT IGraphBuilder::Render(IPin *ppinOut) 这个方法从当前 FilterGraph 的某个 Filter 的指定输出 Pin 开始,进行从这个 Pin 往下的一条支路的智能连接。智能连接的算法和上述 RenderFile 的类似。 HRESULT IGraphBuilder::Connect(IPin *ppinOut,IPin *ppinIn) 这个方法以欲连接的一对输出 Pin 和输入 Pin 作为参数,首先进行这两个 Pin 之间的直接连接。 如果不成功, 则要插入 “中介” Filter。 这个 “中介” Filter 的选择以及试连接过程就是一个智能连接过程, 算法与上述的 RenderFile 类似。 在执行智能连接函数之前,可以首先将我们的 Filter 加入到 FilterGraph 中, 或者在提高一个优先级:将 Filter 加入 FilterGraphManager 的内部缓冲。后一方 法可以从 FilterGraphManager 上获得 IGraphConfig 接口,然后调用其接口方法 AddFilterToCache 来实现。值得注意的是,如果要加入到缓冲中的 Filter 已经在 FilterGraph 中了,则这个 Filter 的所有 Pin 必须都处于断开状态。

    4.4.3 动态重建技术
    由于下列任何一个原因,我们都需要对已有的 FilterGraph 进行修改 1) 应用程序在播放一段视频的过程中想要插入一个视频效果 Filter 2) SourceFilter 在运行过程中改变了数据流的媒体类型,需要接入新的编码 Filter 3) 应用程序想要在 FilterGraph 中加入另外一条视频流 通常的做法是,先将 FilterGraph 停止,进行修改之后,再重新启动。动态 重建能够保持 FilterGraph 运行状态的同时实现 FilterGraph 的重建: 1)仅仅改变 Filter 之间连接的媒体类型; 2)增加或删除 Filter,重新进行相关 Filter 之间的连接; 3)对一条 Filter 链路(Filter Chain)进行操作; 媒体类型的动态改变 当两个 Pin 连接完成后, 就会有一个双方商定的媒体类型用来描述以后在这 两个连接 Pin 之间传送的数据格式。一般情况下,这个媒体类型在 FilterGraph 运行的整个过程中是不会改变的。但是在一些特殊的场合下, “需要”这种媒体 类型的动态改变。 从上往下要求媒体类型改变,但传送数据使用的内存不需要增大 媒体类型的改变由 FilterA 的输出 Pin 发起。首先,Filter A 调用 Filter B 的 输入 Pin 上的 IPin::QueryAccept 或 IPinConnetion::DynamicQueryAccept,看 Filter B 是否支持新的媒体类型。 如支持, 则 Filter A 调用 IMemAllocator::GetBuffer 得到 一空 Sample,通过 IMediaSample:: SetMediaType 将新的媒体类型与 Sample 相连。 一般来说,Filter B 应该具有对媒体类型改变的应变能力,这是在写自己的

    Filter 时应该考虑到的问题。Filter A 一般调用 QueryAccept 来判断 Filter B 的输 入 Pin 是否支持新的媒体类型。可以从 CBasePin::QueryAccept 的实现中看到, 它仅仅是调用 Pin 上的 CheckMediaType

    函数。 所以 QueryAccept 返回 S_OK 并不能保证媒体类型能够在 Graph 运行状态 下改变成功,另一种可靠的方法是,如果 Filter B 的输入 Pin 支持 IPinConnection 接口,那么调用这个接口的 DynamicQueryAccept 方法。 对于 Filter 开发人员来说,还需注意一下 CBaseInputPin::Receive 的实现,代 码如下: STDMETHODIMP CBaseInputPin::Receive(IMediaSample*pSample) { CheckPointer(pSample,E_POINTER); ValidateReadPtr(pSample,sizeof(IMediaSample)); ASSERT(pSample);//上级输出 Sample //检查 Filter 内部状态,以及数据流传送状态 HRESULT hr=CheckStreaming(); if(S_OK!=hr){ return hr;} //查询 IMediaSample2 接口,获取输入 Sample 上的属性 IMediaSample2 *pSample2;//本级输入 Sample if(SUCCEEDED(pSample->QueryInterface(IID_IMediaSample2,(void**)&pSamp le2))){ hr =pSample2->GetProperties(sizeof(m_SampleProps),(PBYPE)&m_SampleProps); pSample2->Release(); if(FALED(hr)){ return hr;}} else{ //使用 IMediaSample 接口获取输入 Sample 上的属性 m_SampleProps.cbData =sizeof(m_SampleProps); m_SampleProps.dwTypeSpecificFlags=0; m_SampleProps.dwStreamId =AM_STREAM_MEDIA; m_SampleProps.dwSampleFlags = 0; if(S_OK = =pSample->IsDiscontinuity()){

    m_SampleProps.dwSampleFlags|=AM_SAMPLE_DATADISCONTIN UITY;} if(S_OK = =pSample->IsSyncPoint()){ m_SampleProps.dwSampleFlags|= AM_SAMPLE_SPLICEPOINT;} if(S_OK = =(SUCCEEDDED(pSample->GetTime(&m_SampleProps.tStart, &m_SampleProps.tStop)) ){ m_SampleProps.dwSampleFlags|=AM_SAMPLE_TIMEVALID| AM_SAMPLE_STOPVALID;} if(S_OK = =pSample->GetMediaType(&m_SampleProps.pMediaType)){ m_SampleProp.dwSampleFlags|=AM_SAMPLE_TYPECHANGED;} pSample->GetPointer(&m_SampleProps.pbBuffer); m_SampleProps.lActual = pSample->GetActualDataLength(); m_SampleProps.cbBuffer = pSample->GetSize(); } //检测 Sample 上的媒体类型是否改变 if(!(m_SampleProps.dwSampleFlags& AM_SAMPLE_TYPECHANGED)){ return NOERROR;} //检查输入 Pin 上是否支持这种新的媒体类型。此时,输入 Pin 应该是支 持这种媒体 //类型的,因为上一级 Filter 在这种媒体类型的动态改变之前,一般会先 调用输入 Pin //上的 IPin::QueryAccept 接口方法进行确认 Hr= CheckMediaType((CMediaType *)m_SampleProps.pMediaType); If(hr == NOERROR){ Return NOERROR;} M_bRunTimeError = TRUE; EndOfStream(); m_pFilter->NotifyEvent(EC_ERRORABORT,VFW_E_TYPE_NOT_AC CEPTED,0); return VFW_E_INVALIDATETYPE;} 我们可以看到, 这个函数首先调用 CheckStreaming 来检查 Filter 的运行状态, 其次就是读取 Sample 的属性(保存到成员变量 AM_SAMPLE2_PROPERTIES m_SampleProps 中) ,判断媒体类型是否已经改变,如果已改变,会重新调用 CheckMediaType 进行检查。这就是 SDK 基类提供给我们的(也是仅有的) ,对 媒体类型动态改变的“免疫能力” 。其实,对于媒体类型动态改变的支持,还需 要处理更多的细节问题(如可能需要重新初始化) ,这要因具体的 Filter 任务而 定。 从下往上要求媒体类型改变,但传送数据使用的内存不需增大,Filter B 必 须拥有自己的 Sample 分配器 媒体类型的改变由 Filter B 的输入 Pin 发起。首先,Filter B 调用 Filter A 的 输出 Pin 的 QueryAccept,看是否支持新的媒体类型。如果支持,则 Filter B 内部 通过私有方法来改变 Sample 管理器中下一个空 Sample 的媒体类型(因为这个 分配器是 Filter B 创建的,所以 Filter B 有方法对下一个空 Sample 调用

    IMediaSample::SetMediaType, 而 无 需 通 过 公 有 接 口 方 法 IMediaAllocator::GetBuffer 先将下一空 Sample 取出) 。接着当 Filter A 调用 IMediaAllocator

    ::GetMediaType 来得到新的媒体类型。此时,FilterA 必须根据新的媒体类型产生 Sample 数据,然后再传给 Filter B。最后,FilterB 在接收到 Sample 时,会确认 媒体类型的改变。 如果 Filter A 能够接受新的媒体类型,那么它也应该有能力恢复到原来的媒 体类型。这种媒体类型的动态改变,最常见于传统的 Video Renderer 上。 输出 Pin 往下要求新的媒体类型,而且需要使用更大的内存以传送新的 Sample 在上述的两种情形中,虽然媒体类型改变了,但是 Sample 使用的内存大小 并不需要扩大。 当新的媒体类型要求更大的内存用于传送数据时, 上一级的 Filter 必须进行如下处理: (1) 调用下一级 Filter 的输入 Pin 的 IPin::ReceiveConnection.如果成功返回, 且 需 要 改 变 Sample 内 存 大 小 , 则 在 输 出 Pin 上 调 用 IMemAllocator::SetProperties,以使用新的内存大小进行 Sample 内存的 重新分配。 (2) 调用输入 Pin 上的 IMemInputPin::NotifyAllocator 通知使用新的 Sample 分配器。 对于 Filter 开发人员来说,还需要注意的是,在做出上述改变之前,确信已 经将所有旧媒体类型的 Sample 都发送出去了。一些 MPEG 视频解码 Filter 就是 使用这种机制来实现输出 MPEG-1 与 MPEG-2 之间的切换,或者视频图象大小 的改变的。 动态删除或增加 Filter 假设我们想将 Filter2 动态移走

    要实现动态删除或增加 Filter 的两个必要条件: (1)Filter3(Pin D)必须支持 IPinConnection 接口 (这个接口保证 Filter 在非停止状态下也能进行 Pin 的重连) ; (2) Filter 在重连的时候不允许数据的传输,所以要阻塞数据线程。如果“重连”

    是由 Filter1 发起的(在 Filter 内部完成) ,那么 Filter1 要同步这个数据发送线程; 如果 “重连” 是由应用程序完成, 则要求 Filter1(PinA)实现 IPinFlowControl 接口。 (1) 在 Filter1 上阻塞数据流线程。IPinFlowControl::Block 可以工作在同步和 异步两种模式下。不能在应用程序主线程下使用该 Block 函数的同步模 式,因为这样可能会引起死锁 (Dead-Block). 应该另外使用一个工作者线 程,或者使用 Block 函数的异步模式。 //创建一个同步时间对象 HANDLE hEvent =CreateEvent(NULL,FALSE,FALSE,NULL); If(hEvent !=NULL){ //阻塞数据流线程 Hr= pFlowControl->Block(AM_PIN_FLOW_CONTROL_BLOCK,hEvent); If(SUCCEEDED(hr)){ //等待完成阻塞工作 DWORD dwRes =WaitForSingleObject(hEvent,dwMilliseconds);}} (2) 重 连 Pin A 和 D , 必 要 时 插 入 新 的 Filter 。 Pin 的 重 连 可 以 使 用 IGraphConfig::Reconnect 或 IGraphConfig::Reconfigure(IGraphConfig 接口 可以从 Filter Graph Manager 上获得)。Reconnect 比 Reconfigure 使用起来 要简单,主要做以下几件事:将新加入的 Filter 置于暂停或者运行状态, 以使其与所在的 FilterGraph 同步。 pGraph->AddFilter(pNewFilter,L”New Filter for the Graph”); pConfig->Reconnect( pPinA,// Reconnect this output pin… pPinD,// …to this input pin pMediaType,//Use this media type pNewFilter,//Connect them through this filter NULL,0); 在实际应用中,如果觉得 Reconnect 不够灵活,还可以改用 Reconfigure.使 用方法时,必须在应用里实现 IGraphConfigCallBack 接口;在调用 Reconfigure 之前,还必须依次调用 Filter3(PinD)上的 IPinConnection::NotifyEndOfStream 和 Filter2(PinB)上的 IPin::EndOfStream,以使得还没有处理完的数据全部发送下去。 ( 3 ) 再 次 启 动 Filter1(PinA) 上 的 数 据 发 送 线 程 。 只 需 要 调 用 IPinFlowControl::Block: pFlowControl->Block(0,NULL); Filter Chain 操作 Filter Chain 是相互连接着的一条 Filter 链路,并且链路中的每个 Filter 至 多有一个处于“已连接”状态的输入 Pin,至多有个处于“已连接”状态的输出 Pin。这条 Filter 链路中的数据流不依赖链路外的其他 Filter。图中的 A-B,C-D, F-G-H,F-G,G-H 都是 Filter Chain,而任何含有 E 的都不是 Filter Chain.

    IFilterChain 接口方法

    StartChain 将指定起始 Filter 和结束 Filter 的 FilterChain 置于运行状态 StopChain 将指定起始 Filter 和结束 Filter 的 FilterChain 置于停止状态 RemoveChain 将指定起始 Filter 和结束 Filter 的 FilterChain 中的所有 Filter 从 FilterGraph 中删除 PauseChain 将指定起始 Filter 和结束 Filter 的 FilterChain 置于暂停状态 没有一个专门的接口方法可以加入或者连接一个 FilterChain。 Filter Chain 的 概念是通过 IFilterChain 的接口方法的两个参数 (一个指定起始的 Filter,一个指定 的结束的 Filter)来体现的。 在使用 IFilterChain 接口前, 还需要确认要操作的 Filter 是否支持 FilterChain 操作,否则很容易引起死锁。一般为了使用 FilterChain 特性,需要特殊设计一些 Filter.

    4.5 Filter 的数据传送

    空闲的 Sample 队列

    Filter 之间的成功连接为数据传送做好了准备, 前面已经提及 Filter 之间是 以 Sample 的形式来传送数据的。 Sample 是一个封装了一定大小数据内存的 COM 组件。每个 Pin 上都实现了 IPin 接口,但这个接口主要是用于 Pin 连接的,而不 是用于数据传送的。 真正用于数据传送的一般是输入 Pin 上实现的 IMemInputPin 接口(调用其接口方法 IMemInputPin:: Receive) ; 连接着双方 Pin 拥有同一个 Allocator(Sample 分配器) ;Allocator 创建,管 理一个或多个 Sample.数据传送时,上一级 Filter 从输出 Pin 的 Allocator 中(调 用 IMemAllocator:: GetBuffer)得到一个空闲 Sample,然后得到 Sample 的数据内 存地址, 将数据放入其中。 最后再将这个 Sample 传送给下一级 Filter 的输入 Pin。 数据从上一级 Filter 输出 Pin 传送到下一级 Filter 的输入 Pin,并没有进行数据的内 存备份,而只是数据到达的“通知” 。典型的数据内存备份发生在 Filter 内部, 从这个 Filter 的输入 Pin 复制到 Filter 的输出 Pin. 数据传送主要有两种模式:推模式(Push Model)和拉模式(Pull Model) 推模式 推模式最典型的情况发生在 LiveSource(实时源,如采集卡的 Capture Filter 等)中。这种源能够自己产生数据,并且使用专门的线程将这些数据“推”下去。 数 据 从 Capture Pin 出 来 , 调 用 AVI Decompressor 的 输 入 Pin 上 的 IMemInputPin::Receive 函数,实现数据从 Caputre Filter 到 AVI Decompressor 的 传送;然后,在 AVI Decompressor 内部,输入 Pin(在自己的 Receive 函数中) 接收到数据后,Filter 将这块数据进行格式转换,再将转换后的数据放到输出 Pin

    的 Sample 中,调用 Null Renderer 的输入 Pin 上的 IMemInputPin::Receive 函数, 从而实现数据从 AVI Decompressor 到 Null Renderer 的传递;Null Render(在输 入 Pin 的 Receive 函数中)接收到数据后进行必要的处理后就返回。至此,数据 传送的一个轮回就完成了。

    拉模式

    拉模式最典型的情况发生在 FilterSource(文件源)中,此源管理着数据,但她 没有把数据“推”下去的能力,而要靠后面的 Filter 来“拉” 。Source Filter(图 中显示为 aviDemo.AVI)的输出 Pin 上一定实现了一个 IAsyncReader 接口(当然 此时 AVI Splitter 也没有必要在输入 Pin 上实现 IMemInputPin 接口) 。 AVI Splitter 的输入 Pin 上一般会有一个“拉”数据的线程,不断调用 SourceFilter 的输出 Pin 上的 IAsyncReader 接口方法来取得数据。 在 AVI Splitter 内部, 将从 Source Filter 中取得的数据进行分析, (音视频)分离,然后分别通过各个输出 Pin 发送出去。 值得注意的是,AVI Splitter 的输出 Pin 往下的数据传送方式,与上述模式是相同 的,即通过调用下一级 Filter 的输入 Pin 上的 IMemInputPin::Receive 函数。

    调用 IAsyncReader 接口方式

    从数据传送所使用的接口看来,Pin 之间可能以 IMemInputPin 连接,也可 能以 IAsynReader 连接。另外,还有一种连接方式,即通过 IOverlay 接口。传统 的 Video Render 就支持这种方式(事实上,Video Render 的输入 Pin 上同时实 现了 IMemInputPin 和 IOverlay 接口,不过这两个接口不能同时使用) 。Capture Filter(VP Pin)->Overlay Mixer2->Video Renderer 这条链路中,Capture Filter 使用 DirectDraw Overlay 方式进行显示,所在在 Overlay Mixer2 与 Video Renderer 之 间并没有实际的数据传输;Video Renderer 此时只相当于一个视频窗口的管理 器。 DirectShow 总是使用专门的线程来传送数据。所以也可以说 DirectShow 应 用程序至少包含两条线程,一条应用程序主线程以及至少一条数据传送子线程。 既然是多线程程序,就不可避免地会出现线程同步问题。

    4.6 Filter 的状态转换
    Filter 有三种:停止,暂停和运行。其中,暂停可以理解为数据就绪状态, 是为了快速切换到运行状态而设计的,作为一种中间状态,暂停也是停止与运行 之间切换所毕经的一种状态。 每个 Filter 上都实现了 IBaseFilter 接口, 而 IBaseFilter 是从 IMediaFitler 继承 而来的。Filter Graph Manager 正是通过 IMediaFilter 的接口方法来控制 Filter 的 状态转换的。 实际上, FilterGraph Manager 内部的实现, 也是分别调用 FilterGraph 内所有 Filter 上的 IMediaFilter::Run,IMediaFilter::Pause 和 IMediaFilter::Stop 完成 的。 对于大多数 Filter 来说(Source Filter 和 Transform Fitler),暂停和运行两种状 态的区别是可以忽略不计的,它们的任务仅仅是以最快的速度处理完数据,然后 把数据往下传送。而对 Rendering Filter(Video Renderer)来说,情况要复杂一点。 Source Filter->Transform Filter->Rendering Filter 其中, Source Filter 是一个普通的工作在推模式下的 Filter (暂不讨论 Live Source) 。 当 Filter Graph 从停止状态向暂停状态时,Source Filter 启动数据线程,并调用 Transform Filter 的输入 Pin 上的 IMemInputPin::Receive 方法,将数据以 Sample 形式往下传送。典型情况下,当 Transform Filter 接收到 Sample 并处理它之后, 会在 Source Filter 的数据线程中将 Sample 继续往下传给 Rendering Filter.当然, Transform Filter 也可以创建自己的工作者线程来继续完成数据的传送。此时, Rendering Filter 已经处于暂停状态,当它接收到第一个 Sample 的时候,就将数 据线程阻塞住。如果是 Video Renderer,它还会将这个 Sample 帧显示在视频窗口。

    当 Filter Graph 从暂停状态转向运行状态时,Rendering Fitler 只需要简单的 将数据线脱离阻塞状态,就可以让整个 Filter Graph 继续运转起来。 Video Renderer 在这种情况下会首先显示在暂停状态下阻塞接收的 Sample 帧,然后每 当接收到新的 Sample 时,将根据 Sample 上打的时间戳来安排显示。如果 Filter Graph 此时的参考时间还没有到当前接收到的 Sample 上的开始时间,则 Video Renderer 会将当前的 Sample 在输入 Pin 的 Receive 函数中 (或者在另外的接收线 程接收下一个新 Sample 时)阻塞住,直到到了当前 Sample 上的开始时间。 如果 Source Filter 是一个 Live Source(如视频采集卡的 Capture Filter)等, 情况就有所不同了。因为这种源一般是通过实时采集产生的数据的,暂停时阻塞 了数据传送线程,在此期间采集的数据因为“过时” ,自然就失去了“实时”的 意义。如果从暂停到运行之间等待了很长一段时间,那么把这段时间内采集的数 据都缓存没有意义。其次,在 DierctShow 设计中,Live Source 并不要求在进入 暂 停 状 态 时 就 启 动 数 据 传 送 。 Filter Graph Manager 调 用 Filter 上 的 IMediaFilter::GetState,如果返回 VFW_S_CANT_CUE,就认为该 Filter 已经成功转 入暂停状态,此时,Video Renderer 在暂停状态下接收不到任何 Sample. Filter Graph Manager 对 Filter 一个一个实现状态转换的顺序很重要。处理不 好,很容易造成 Sample 的丢失或者 Filter Graph 的死锁。因此,DirectShow 设计 了一种从下往上的”回朔”方法。Filter Graph Manager 总是从 Rendering Filter 开 始执行状态转换处理,沿着连接着的 Filter 链路,最后才处理到 Source Filter.最 典型的状态转换发生在停止和暂停之间: 停止->暂停 首先从 Rendering Filter 开始进行暂停状态的转换。这时,Filter 调用自己所 有 Pin 上的 Active 函数进行初始化(一般 Pin 在 Active 函数调用中进行 Sample 内存的分配。如果是 Source filter,还将启动数据线程) ,使 Filter 处于一种就绪状 态。Source Filter 是最后一个完成到就绪状态转换的 Filter。当 Source Filter 启动 数据线程后, 就开始往下发送 Sample;而当 Rendering Filter 接收到第一个 Sample 后就阻塞住, 真正完成了状态转换, 即都接收到执行暂停后的第一个 Sample (上 文论述的 Live Source 情况除外) ,Filter Graph Manager 才能认为状态转换完成。 暂停->停止 当 Filter 进入停止状态时,会调用自己所有 Pin 上的 Inactive 函数(一般 Pin 在 Inactive 函数调用中进行 Sample 内存的释放。 如果是 Source Filter,还将终止数 据线程) 。各个 Filter 内部的处理还包括:释放所有阻塞住的 Sample,以使上一 级 Filter 的 IMemAllocator::GetBuffer 调用脱离阻塞;终止所有在 Receive 函数中 的等待,以使上一级 Filter 的 Receive 函数调用返回。另外,Filter 在停止状态下 拒绝接收任何 Sample.就这样从 Rendering Filter 往上一级一级的脱离阻塞。当到 达 Source Filter 的时候,可以确保数据线程的成功停止。 可以看到,Filter 在实现状态转换过程中,要处理一系列的函数调用。因为 DirectShow 应用程序肯定是多线程程序 (一条应用程序主线程和至少一条数据传 送子线程) ,我们下面来看下 Filter 中的线程同步问题。 在 SDK 基类源代码 CBaseFilter 的定义中,用于标志 Filter 状态的成员变量 的代码如下: FILTER_STATE m_State; 其中,Filter_State 是一个枚举类型,定义如下: Typedef enum_FilterState{

    State_Stopped =0,//停止状态 State_Paused =State_Stopped+1,//暂停状态 State_Running =State_Paused+1//运行状态} FILTER_STATE; 还有如下用于同步 Filter 对象状态的同步对象指针的代码: CCritSec *m_pLock; 然而,在 CBaseFilter 上找不到用于同步数据流线程操作的同步对象,但是 可以从 CTransformFilter 类的定义中找到答案: CCritSec m_csFilter;//Filter 状态对象实例 CCritSec m_csReceive;//数据流线程同步对象实例 CCritSec 是 SDK 提供的 CRITICAL_SECTION 的封装类,可用于线程同步, 另外 SDK 还提供一个类 CAutoLock,能够对 CCritSec 对象在一定的作用域内自动 加锁,解锁: CCritSec csMyLock;//未锁定状态 { CAutoLock cObjectLock(&csMyLock);//自动锁定 //执行受保护代码}//超出 cObjectLock 对象的作用域, cObjectLock 对象析构, 自动解锁。 为了防止资源使用冲突,必须使用上述同步对象使各个函数调用“串行化” 。 在应用程序主线程中调用的函数,必须使用 Filter 状态同步对象进行同步;在数 据线程中调用的函数,必须使用数据流同步对象进行同步。两者不能混淆,否则 很容易一起线程死锁。 媒体定位的实现 应用程序可以 Filter Graph Manager 上获得的 IMediaSeeking 接口, 实现对流 媒体的随机定位(Seeking).实际上,IMediaSeeking 接口的真正实现在 Filter 上。 应用程序通过 Filter Graph Manager 上申请获得 IMediaSeeking 接口执行定位操作 (即调用 IMediaSeeking 的各接口方法)时, Filter Graph Manager 会首先从 Renderer Filter 上请求这个操作的执行。 一般 Renderer Filter 不能真正执行定位操 作, 因此它将定位操作请求发给上一级 Transfrom Filter 的输出 Pin (通过 Renderer Filter 的输入 Pin 可以获得 Transform Filter 的输出 Pin 的指针,然后在这个输出 Pin 上调用 QueryInterface 得到 IMediaSeeking 接口,再调用对应的接口方法) 。 Transform Filter 如果能完成这个定位操作,则在这个 Filter 内执行后返回;如果 不支持,则继续向上一级 Filter 的输出 Pin 请求,知道请求到 Source Filter.

    实际上,一般在推模式的 Source Filter 的输出 Pin 上,或者拉模式下(Source

    是标准的文件源)的 Parser Filter 或 Splitter Filter 的输出 Pin 上才可以真正执行定 位操作。 对于 Filter 开发人员来说, 如果写的是推模式 Source Filter, 一般应该在 Filter 的输出 Pin 上实现 IMediaSeeking 接口;如果写的是 Transform Filter,只需要在输 出 Pin 上将用户的定位操作请求向上传递给上一级 Filter 的输出 Pin;而如果写的 是 Renderer Filter,就需要在 Filter 上 (因为 Renderer Filter 没有输出 Pin)将用户的 定位操作请求向上传递给上一级 Filter 的输出 Pin.另外,为了保证定位操作后媒 体流的同步,如果真正实现 IMediaSeeking 各接口方法的 Filter 有多个输出 Pin, 一般仅让其中一个 Pin 支持定位操作。 对于定制的 Transform Filter,如果有多个输 出 Pin,就需要自己决定当输出 Pin 接收到定位操作请求时, 选择哪一条路径向上 继续请求。 可 以 借 助 SDK 基 类 源 代 码 CSourceSeeking, CPosPassThru, CRenderer-PossPassThru 等几个类(参见 ctlutil.cpp 文件)实现 Filter 上的媒体 的随机定位。 另外, 写 Transform Filter 时, 如果 Filter 父类采用 CTransfromFilter 或 CtranslnPlaceFilter,那就不用考虑支持 IMediaSeeking 接口问题,因为基类中 已经实现好了: STDMETHODIMP CTransformOutputPin::NonDelegationQueryInterface(REFIID riid, void **ppv) { CheckPointer(ppv,E_POINTER); ValidateReadWriterPtr(ppv,sizeof(PVOID)); *ppv=NULL; if(riid ==IID_IMediaPosition||riid ==IID_IMediaSeeking){ ASSERT(m_pTransformFilter->m_pInput!=NULL); if(m_pPosition ==NULL){ //创建能够将定位操作请求直接往上传递的 COM 组件 HRESULT hr=CreatePosPassThru( GetOwner(),FLASE,(IPin *)m_pTransformFilter->m_pInput,&m_pPosition); if(FAILED(hr)){ return hr;}} return m_pPosition->QueryInterface(riid,ppv);} else{ return CBaseOutputPin::NonDelegatingQueryInterface(riid,ppv);}} 应用程序能够在任何时候(停止,暂停或者运行)对 Filter Graph 执行定位操 作。但当 Filte graph 正在运行的时候,Filter Graph Manager 会先将 Filter Graph 暂停,执行完定位操作后,再重新运行。 IMediaSeeking 可以支持如下几种格式的定位操作: TIME_FORMAT_FRRME (默认) //按视频帧定位 TIME_FORMAT_SAMPLE //按 Sample 计数定位 TIME_FORMAT_FIELD //按场定位 TIME_FORMAT_BYTE //按字节流中的偏移定位 TIME_FORMAT_MEDIA_TIME // 按参考时间定位(以 100ms 为单位)

    真正实现 IMediaSeeking 接口方法的 Filter 也可以同时支持其他格式,但未 必 支 持 所 有 的 格 式 。 对 于 应 用 开 发 人 员 来 说 , 当 使 用 除 TIME_FORMAT_MEDIA_TIME 之 外 的 其 他 格 式 时 , 最 好 先 调 用 IMediaSeeking::IsFormatSupported 进行确认。 IMediaSeeking 除了可以进行媒体的随机定位外,还可以用来调整媒体文件 的回放速率,即调用接口方法 IMediaSeeking::SetRate, 如参数为 2.0 表示为 2 倍 速播放,参数为 0.5 表示以原来的一般速率播放。理论上,参数为负数可以实现 “倒播” ;但事实上,绝大部分 Filter 不支持这一特性。 当应用程序执行 IMediaSeeking::SetRate 后,Filter Graph Manager 的内部响 应如下: (1) 调用 IMediaSeeking::GetCurrentPoition 得到当前播放到的位置; (2) 如果当前 Filter Graph 处于暂停或运行状态,则调用 Stop; (3) 调用 IMediaSeeking::SetPositons 将步骤(1)中取得的当前位置重新设置 一下; (4) 调用 IMediaSeeking::SetRate 设置新的回放速率; (5) 如果先前 Filter Graph 的状态为暂停或运行,则恢复这个状态; 相应的,各部分 Filter 上的响应如下: 推模式 Source Filter 或拉模式 Parser/Splitter Filter:这些 Filter 一般都对输出的 Sample 时间打时间戳, 是真正响应 SetRate 的 Filter.当 Filter Graph 执行定位操作 或者回放速率改变后, 它们一般都会调用输出 Pin 上的 DeliverNewSegment 函数, 提示一个新格式的流的开始;当然,此后输出的 Sample 的时间戳应该根据新的 回放速率来计算。 解码 Filter:这种 Filter 一般不用对 SetRate 作出直接的响应,而是在接收到 NewSegment 调用后,把新的参数保存下来,并继续将 NewSegment 调用传递给 下一级 Filter. Renderer Filter: Video Renderer 可以忽略回放速率的改变,因为最初发送 Sample 的 Filter 已经根据新的回放速率调整了时间戳。Video Renderer 仍然只须 根据 Sample 上的时间来安排显示时机。但对于 Audio Renderer 来说,一般须要 做一些相关的回放速率调整。 质量控制的实现 数据线程是通过一种“压迫式”方式往下传送数据的。 虽然 Video Renderer 接收到 Sample 后,会根据 Sample 上的时间戳来正确安排显示时机。 质量控制的实现 数据线程是通过一种“压迫式”方式往下传送数据的。虽然 Video Renderer 接收到 Sample 后,会根据 Sample 上的时间戳来正确安排显示时机。但仅仅这样 是不够的,它不能改善 Filter Graph 运行时的性能。 因此,DirectShow 另外设计了一种“自适应”的反馈机制:质量控制(Quality Control).运用这种机制,当 Video Renderer 发现数据线程发送数据太快时,就能 发送一个消息信息要求 Source Filter(拉模式下一般为 Parser Filter 或 Splitter Filter)减慢速度,反之加快速度: DirectShow 使用如下数据结构来描述这个质量控制消息: Typedef struct{ QualityMessageType Type; Long Proportion;

    REFERENCE_TIME Late; REFERENCE_TIME TimeStamp; }Quality; 其中,Type 定义了消息的类型,取值为如下枚举类型: Typedef enum{ Famine,//表示当前发送数据的速度太慢 Flood //表示当前发送数据的速度太快 }QualityMessageType; Proportion 以 1000 为基数定义了 Video Renderer”建议”的新的数据发送数率。 例如,如果 Proportion 的值为 800,则建议新的发送速率为原速率的 80%;如果 Proportion 的值为 1200,则建议新的发送速率为原速率的 120%。 Late 定义了发送这个消息时, 在 Video Renderer 上的 Sample 实现晚了多少时 间(以 100ns 为单位) 。TimeStamp 定义了发送这个消息时,在 Video Renderer 上的 Sample 的时间戳的开始时间。 质量控制通过 IQualityControl 接口来实现,接口方法有: Notify 发送质量控制消息 SetSink 设置一个自定义的质量控制处理器 质量控制在 Filter 上具体实现:

    3.Notify 2.PassNotify

    1.Notify

    从图中可以看到,质量控制消息最初由 Video Renderer 发出,经过 Transform Filter 的输出 Pin,询问 Transform Filter 是否能够进行质量的改善;如果不能,则 继续往上传递,直到 Source Filter 的输出 Pin 上。 在 SDK 基类的实现中还可看 到,CBasePin 上实现 IQualityControl 接口,可以通过 Pin 来实现质量控制消息的 传递,而且默认的实现代码为: STDMETHODIMP CBasePin::Notify(IBaseFilter *pSender,Quality q) { UNREFERENCED_PARAMETER(q); UNREFERENCED_PARAMETER(pSender); DbgBreak(“IQualityControl::Notify not over-ridden from CBasePin.(IGNORE is OK)”); return E_NOTIMPL;} 返回 E_NOTIMPL 表示没有实现真正的质量控制处理 (留给派生类去实现) 。 而 CTransformFilter 的输出 Pin 实现代码如下: STDMETHODIMP CTransformOutputPin::Notify(IBaseFilter *pSender,Quality q) { UNREFRENCED_PARAMETER(pSender); ValidateReadPtr(pSender,sizeof(IBaseFilter));

    //调用 Filter 上的 AlterQuality 函数,看是否能够执行质量控制 //返回 S_FALSE 表示该 Filter 不能处理质量控制 HRESULT hr=m_pTransformFilter->AlterQuality(q); if(hr!=S_FALSE){ return hr; //either S_OK or a failure} //S_FALSE means we pass the message on //Find the quality sink for our input pin and send it there ASSERT(m_pTransformFilter->m_pInput!=NULL); //将质量控制消息往上一级 Filter 传递 return m_pTransformFilter->m_pInput->PassNotify(q);} 可以看到,当 Transform Filter 接收到质量控制信息后,首先调用 Filter 的函 数 AlterQuality,询问 Filter 是否可以进行质量的改善(默认实现中,AlterQuality 返回 S_FALSE 表示不进行处理) ,如果不可以,则调用输入 Pin 上的 PassNotify 函数,将质量控制消息传递给上一级 Filter 的输出 Pin. 这是 DirectShow 中一般的质量控制处理过程。另外,应用程序还可以实现自 己的质量控制处理器,然后通过调用 IQualityControl::SetSink 方法设置给 Video Renderer.当然,这个自定义的质量控制处理器必须实现 IQualityControl 接口,并 且通常是 Filter Graph 中的某一个组件对象。设置成功后,上述处理过程就改变 了,质量消息会直接发送给自定义的处理器。但是,值得注意的是,大多数应用 程序不这么做,因为具体实现时还要考虑很多细节。具体质量控制策略的实现取 决于具体的 Filter 实现,可能是调整发送速度,也可能是丢失部分数据,所以, 在实际应用中还要慎重使用质量控制机制, 特别是对那些对数据有严格要求的应 用。

    第五章 DirectShow 应用开发过程
    5.1 开发环境的配置
    DirectShow SDK 建议,所有 DirectShow 应用程序都应包含 Dshow.h 文件, 其实 Dshow.h 中没有什么特别定义,只是嵌套包含了其他一些头文件: //Include standard Windows files #include <window.h> #include <windowsx.h> #include <olectl.h> #include <ddraw.h> #include <mmsystem.h> //Include DirectShow include files #include <strmif.h>//Generated IDL header file for streams interfaces #include <amvideo.h>//ActiveMovie video interfaces and definitions #include <amudio.h>//ActiveMovie audio interfaces and definitions #include <control.h>//generated from control.odl #include <evcode.h>//event code definitions #include <uuids.h>//declaration of type GUIDs and well-known clsids #include <errors.h>//HRESULT status and error definitions

    #include <edevdefs.h>//Extrnal device control interface defines #include <audevcod.h>//audio filter device error event codes #include <dvdevcod.h>//DVD error event codes 不难发现,上述这些头文件在 streams.h 中包含,在实际应用中,我们也常 常用 streams.h 文件来替换 Dshow.h 文件。 需要连接的库文件 DirectShow SDK 建议,DirectShow 应用程序应至少连接库文件 Strmiids.lib 和 Quartz.lib.前者定义 DirectShow 标准的 CLSID 和 IID,后者定义了导出函数 AMGetErrorText(如果应用程序中没有用到这个函数,也可以不连接这个库) 。 如 果 我 们 写 程 序 的 时 候 包 含 了 头 文 件 streams.h, 则 一 般 库 文 件 还 要 连 接 strmbasd.lib(Release 版本为 strmbase.lib),uuid.lib 和 winmm.lib.在 VC 中连接库 文件的方法是: 选择 VC 的菜单项 Project|Settings|Link,在这页中的 Category 选择 General,然后在 Object/library modules 中输入库文件名称。 因为 DirectShow 应用程序是一种 COM 客户程序,因此在调用任何 COM 函 数之前调用 CoInitialize(或 CoInitializeEx)函数进行 COM 库的初始化(一般是 在应用程序启动的时候调用一次) ,在结束 COM 库使用时调用 CoUninitialize 函 数进行反初始化(一般是在应用程序退出前调用一次) 。

    5.2 一般开发过程
    第一阶段,创建一个 Filter Graph Manager 组件,代码如下: IGraphBuilder *pGraph =NULL; HRESULT hr =CoCreateInstance(CLSID_FilterGraph,NULL,CLSCTX_INPROC_ SERVER,IID_IGraphBuilder,(void**)&pGraph); 第二阶段,根据实际的应用,创建一条完整的 Filter 链路。比如播放一个本 地文件,最简单快捷的代码如下: hr =pGraph->RenderFile(L"F:\\id-moai-dvdscr.avi",NULL); 第三阶段,调用 Filter Graph Manager 上(或者直接在某个 Filter 上)的各个 接口方法进行控制,并且完成 Filter Graph Manager 与应用程序的事件交互。比 如调用 IMediaControl 接口方法控制 Filter Graph 的状态转换,代码如下: IMediaControl *pControl=NULL; hr=pGraph->QueryInterface(IID_IMediaControl,(void**)&pControl); hr=pControl->Run();//运行 Filter Graph 在程序中构建完 Filter 链路后,有时候并不会马上能够正常运行,要是能够 “直观”看到在程序中 Filter 是怎么连接的,实际上,GraphEdit 也提供了这样一 种能够显示其他应用程序创建的 Filter Graph 的功能。 在程序中创建了一个 Filter Graph Manager 之后, 应该立即将其注册到活动对象表(Running Object Table,ROT) 中,代码如下: HRESULT AddToRot(IUnknown *pUnkGraph, DWORD *pdwRegister) { IMoniker *pMoniker; IRunningObjectTable *pROT; if(FAILED(GetRunningObjectTable(0,&pROT))){ return E_FAIL;} WCHAR wsz[256];

    wsprintfW(wsz,L"FilterGraph %08x pid %08x",(DWORD_PTR)pUnkGraph, GetCurrentProcessId()); HRESULT hr=CreateItemMoniker(L"!",wsz,&pMoniker); if(SUCCEEDED(hr)){ hr=pROT->Register(ROTFLAGS_REGISTRATIONKEEPSALIVE,pUnkGrap h,pMoniker,pdwRegister);} pMoniker->Release(); return hr;} 之后,我们就可以在 GraphEdit 中连接,显示这个 Filter Graph 了,方法为: 选择 GraphEdit 的菜单项 File|Connect to remote Graph,在弹出的对话框中选择一 个 Filter Graph 标识项后,我们能看到,相应应用程序的 Filter Graph 显示在 GraphEdit 的客户区窗口中了。 当 GraphEdit 用于显示其他应用程序的 Filter Graph 时,请尽量不要使用 GraphEdit 提供的功能编辑 Filter 链路(增加或删除 Filter) , 也不要改变这个 Filter Graph 的运行状态,否则会引起它的应用程序的不可预知 的错误。在我们的应用程序退出前,应该将这个显示我们应用程序创建的 Filter Graph 的 GraphEdit 程序关掉,在程序中析构 Filter Graph Manager 之前,我们还 必须从 ROT 中注销我们先前的注册,代码如下: void RemoveFromRot(DWORD pdwRegister) { IRunningObjectTable *pROT; if(SUCCEEDED(GetRunningObjectTable(0,&pROT))){ pROT->Revoke(pdwRegister); pROT->Release();} } 在应用程序中,整个过程实现的代码如下: IGraphBuilder *pGraph; DWORD dwRegister: //Create the filter graph manager CoCreateInstance(CLSID_FilterGraph,NULL,CLSCTX_INPROC_SERVER,IID_IGr aphBuilder,(void**)&pGraph); #ifdef _DEBUG Hr=AddToRot(pGraph,&dwRegister); #endif //Reset of the application(not shown) #ifdef _DEBUG RemoveFromRot(dwRegister); #endif pGraph->Release();

    5.3 通用 Filter Graph 构建技术
    构建不同的 Filter 链路,取决于具体的应用。为了简化 Filter Graph 的构建, DirectShow 为几个典型的应用提供了辅助的构建组件,比如创建 DVD 播放的 Filter Graph 时, 可以使用 CLSID_DvdGraphBuilder 组件 (提供 IDvdGraphBuilder 接口) ;进行音视频采集时,可以使用 CLSID_CaptureGraphBuilder2 组件(提供

    ICaputreGraphBuilder2 接口) 。

    5.3.1 加入一个指定 CLSID 的 Filter
    给定一个 Filter 的 CLSID,我们就可以使用 COM API 函数 CoCreateInstance 来创建它, 并且使用 IFilterGraph::AddFilter 接口方法将其加入到 Filter Graph 中, 代码如下: HRESULT AddFilterByCLSID( IGraphBuilder *pGraph,//Pointer to the Filter Graph Manager Const GUID & clsid,//CLSID of the filter to create LPCWSTR wszName,//A name for the filter IBaseFilter **ppF)//Receive a pointer to the filter { if(!pGraph||!ppF) return E_POINTER; *ppF =0; IBaseFilter *Pf=0; HRESULT hr=CoCreateInstance(clsid,0,CLSCTX_INPROC_SERVER, IID_IBaseFilter,reinterpret_cast<void**>(&pF)); if(SUCCEEDED(hr){ hr= pGraph->AddFilter(Pf,wszName); if(SUCCEEDED(hr)) *ppF=Pf; else pF->Release(); } return hr; } 我们现在要想 Filter Graph 中加入一个 AVI Mux Filter,代码如下: IBaseFilter *pMux; Hr= AddFilterByCLSID(pGraph,CLSID_AviDest,L”AVI Mux”,&pMux); If(SUCCEEDED(hr)){ /*….*/ pMux->Release();} 并不是所有的 Filter 都能通过 CoCreateInstance 函数来创建,大部分注册在 Audio Compressors 目录下的 Filter(它们使用 CLSID_ACMWrapper 包装 Filter), 大部分注册在 Video Compressors 目录下的 Filter(它们使用 CLSID_AVICo 包装 Filter),以及代表硬件(WDM 或 VFW)的包装 Filter,必须通过枚举的方式来创建。

    5.3.2 得到 Filter 上的未连接 Pin
    当我们在程序中连接 Filter 的时候, 首先是要取得 Filter 上未连接的输入 Pin 或者输出 Pin。方法是,枚举 Filter 上的所有 Pin,调用 IPin::QueryDirection 查看 Pin 的方向,调用 IPin::ConnectedTo 查看 Pin 的连接状态。代码如下: HRESULT GetUnconnectedPin( IBaseFilter *pFilter,//Pointer to the filter PIN_DIRECTION PinDir,//Direction of the pin to find

    IPin **ppPin) //Receives a pointer to the pin { *ppPin=0; IEnumPins *pEnum=0; IPin *pPin=0; HRESULT hr=pFilter->EnumPins(&pEnum) ; if(FAILED(hr) ) { return hr; } while(pEnum->Next(1,&pPin,NULL)==S_OK) { PIN_DIRECTION ThisPinDir; pPin->QueryDirection(&ThisPinDir) ; if(ThisPinDir==PinDir) { IPin *pTmp=0; hr=pPin->ConnectedTo(&pTmp) ; if(SUCCEEDED (hr) )//Already connected, not the pin we want { Pimp->Release() ; } else //Unconnected,this is the pin we want { pEnum->Release() ; *ppPin=pPin; return S_OK; } } pPin->Release() ; } pEnum->Release() ; //Did not find a matching pin return E_FAIL; } 比如我们现在要得到一个 Filter 的未连接的输出 Pin,代码如下: IPin *pout = NULL; HRESULT hr=GetUnconnectedPin(pFilter,PINDIR_OUTPUT,&pout) ; if(SUCCEEDED(hr) ) { … Pout->Release() ; }

    5.3.3 连接两个 Filter

    两个 Filter 的连接, 总是从上一级 Filter 的输出 Pin 指向下一级 Filter 的输入 Pin 。 Filter Graph Manager 提 供 的 直 接 连 接 两 个 Filter 的 函 数 为 IFilterGraph::ConnectDirect 和 IGraphBuilder::Connect。参考代码如下: HRESULT ConnectFilters( IGraphBuilder *pGraph,//Filter Graph Manager IPin *pOut,//Output pin on the upstream filter IBaseFilter *pDest)//Downstream Filter { if ((pGraph ==NULL)||(pout==NULL)||(pDest==NULL)) { return E_POINTER; } #ifdef debug PIN_DIRECTION PinDir; pOut->QueryDirection(&PinDir) ; _ASSERTE(PinDir==PINDIR_OUTPUT) ; #endif //得到下一级 Fi1ter 的输入 Pin IPin *pIn = 0; HRESULT hr=GetUnconnectedPin(pDest,PINDIR_INPUT,&pIn) ; if (FAILED (hr)) { return hr; } //将输出 Pin 连接到输入 Pin hr=pGraph->Connect(pOut,pIn) ; pIn->Release() ; return hr; } //不同参数的 ConnectFilters 函数重载形式 HRESULT ConnectFilters( IGraphBuilder *pGraph, IBaseFilter *pSrc, IBaseFilter *pDest) { if((pGraph==NULL)|| (pSrc==NULL)|| (pDest==NULL)) { return E_POINTER; } //Find an output pin on the first filter IPin *pout=0; HRESULT hr=GetUnconnectedPin(pSrc,PINDIR_OUTPUT,&pOut) ; if(FAILED(hr) ) {

    return hr; } hr=ConnectFilters(pGraph,pOut,pDest) ; pout->Release() ; return hr; } 比如我们现在要加入一个 AVI Mux Filter 和一个 File Writer Filter,然后将它 们相连起。代码如下: IBaseFilter *pmux,*pWrite; hr=AddFilterByCLSID(pGraph,CLSID_AviDest,L“AVI Mux” , &pMux) ; if(SUCCEEDED(hr)) { hr = AddFilterByCLSID(pGraph , CLSID_FilterWriter, L “ File Writer ” , &pWrite); if(SUCCEEDED(hr)) { hr = ConnectFilters(pGraph, pMux, pWrite); /* Use IFileSinFilter to set the file name (not shown). */ pWrite->Release(); } pMux->Release(): }

    5.3.4 查找 Filter 或 Pin 上的接口
    大部分情况下, 我们使用 Filter Graph Manager 上的接口来操作 Filter Graph。 但有些时候,我们也需要直接获得 Filter 或者它的 Pin 上的某个接口进行参数配 置等。 如果 Filter Graph 中的 Filter 都是我们一个一个加进去的, 而且我们知道自己 需要的接口具体在哪个 Filter 上实现,我们当然可以直接调用这个 Filter 或它的 Pin 上的 QueryInterface 函数来得到我们想要的接口。 但问题是,如果上述条件不满足,我们该怎么办呢? 于是,我们就要枚举 Graph 中的所有 Filter,或者枚举 Filter 上的所有 Pin,一个一个地找。代码如下: HRESULT FindFilterInterface( IGraphBuilder *pGraph, //Pointer to the Filter Graph Manager REFGUID iid, //IID of the interface to retrieve void **ppUnk) //Receives the interface pointer { if(!pGraph || !ppUnk) return E_POINTER; HRESULT hr = E_FAIL; IEnumFilter *pEnum = NULL; IBaseFilter *pF = NULL; if ( FAILED(pGraph->EnumFilters(&pEnum))) { return E_FAIL;

    } //分别调用 QueryInterface,枚举 Filter Graph 中的所有 Filter while ( S_OK ==pEnum->Next(1, &pF, 0)) { hr = pF->QueryInterface ( iid, ppUnk); pF->Release(); if(SUCCEEDED(hr)) { break; } } pEnum ->Release(); return hr; } //查找给定 Filter 的 Pin 上实现的某个接口 HRESULT FindInterface( IBaseFilter *pFilter, //Pointer to the filter to search REFGUID iid, //IID of the interface void **ppUnk) //Receive the interface pointer { if ( !pFilter || !ppUnk) return E_POINTER; HRESULT hr = E_FAIL; IEnumPins *pEnum = 0; if(FAILED(pFilter->EnumPins(&pEnum))) { return E_FAIL; } //分别调用 QueryInterface,枚举 Filter 上的所有 Pin IPin *pPin = 0; while ( S_OK == pEnum->Next(1, &pPin, 0)) { hr = pPin->QueryInterface( iid, ppUnk); pPin->Release(); if(SUCCEEDED(hr)) { break; } } pEnum->Release(): return hr; } // 综合 FindFilterInterface 和 FindPinInterface 两个函数的功能 HRESULT FindInterfaceAnyWhere( IGraphBuilder *pGraph,

    REFGUID iid, void **ppUnk) { if (!pGraph || !ppUnk) return E_POINTER; HRESULT hr = E_FAIL; IEnumFilters *pEnum = 0; if( FAILED(pGraph->EnumFilters(&pEnum))) { return E_FAIL; } // Loop through every filter in the graph IBaseFilter *pF = 0; while ( S_OK == pEnum->Next(1, &pF, 0)) { hr = pF->QueryInterface(iid, ppUnk); if(FAILED(hr)) { //the filter does not expose the interface, but maybe one of its pins does hr = FindPinInterface(pF, iid, ppUnk); } pF->Release(); if(SUCCEEDED(hr)) { break; } } pEnum->Release(); return hr; } 比如我们现在通过 IGraphBuildecRendefile 函数,构建了一条内含 DV 格式 数据的 AVI 文件的回放链路。我们想要得到其中 DV 视频解码 Filter 上的 IIPDVDec 接口,以设置 DV 解码输出的图像大小为原始图像的四分之一。实现 代码如下: hr=pGraph->RenderFile(L“C:\\Example.avi” ,0) ; if(SUCCEEDED(hr) ) { IIPDVDec *pDvDec; hr = FindFilterInterface(pGraph,IID_IIPDVDec, (void**)&pDvDec) ; if( SUCCEEDED(hr) ) { pDvDec->put_IPDisplay(DVRESOLUTION_QUARTER) ; pDvDec->Release() ; } else if(hr==E_NOINTERFACE)

    { //This file does not contain DV video } else { //Some other error occurred } }

    5.3.5 遍历 Filter 链路
    给定一条 Filter 链路中的某个 Filter, 我们可以往上或往下遍历得到所有的其 他 Filter.方法是: 从本级 Filter 的输入 Pin 或者输出 Pin,可以得到上一级或下一级 Filter 的连接着的 Pin, 再调用这个 Pin 的 lPin::QueryPinlnfo 就可以得到它的拥有 者 Filter。 重复上述过程, 我们就可以一直遍历到 Source Filter 或者 Renderer Filter. 给定一个 Filter 得到它的往上或往下相连的 Filter 的函数的代码如下: HRESULT GetNextFilter( IBaseFilter *pFilter,//遍历开始的 Filter PIN_DIRECTION Dir,//遍历方向 IBaseFilter**ppNext)//得到的相邻 Filter { if(!pFilter || !ppNext)return E_POINTER; IEnumPins *pEnum=0; IPin *pPin=0; HRESULT hr=pFilter->EnumPins(&pEnum);//枚举 Filter 上所有 Pin if(FAILED(hr) )return hr; while(S_OK==pEnum->Next(1,&pPin,0) ) { //See if this pin matches the specified direction PIN_DIRECTION ThisPinDir; hr=pPin->QueryDirection(&ThisPinDir) : if(FAILED(hr) ) { //Something strange happened. hr=E_UNEXPECTED; pPin->Release() ; break; } if(ThisPinDir==Dir) { //Check if the pin is connected to another pin IPin *pPinNext=0; hr=pPin->ConnectedTo(&pPinNext) ; if(SUCCEEDED(hr) ) {

    //Get the filter that owns that pin PIN_INFO PinInfo; hr=pPinNext->QueryPinInfo(&PinInfo) ; pPinNext->Release() ; pPin->Release() ; pEnum->Release() ; if(FAILED(hr)||(PinInfo.pFilter==NULL) ) { //Something strange happened. return E_UNEXPECTED; } //This is the filter we’re looking for *ppNext=PinInfo.pFilter;//Client must release return S_OK; } } pPin->Release() ; } pEnum->Release() ; //Did not find a matching filter return E_FAIL; }

    5.3.6 成批删除 Filter
    删除 Filter Graph 中的所有 Filter 的代码如下: //首先停止 Filter Graph 的运行 pControl->Stop() ; //枚举 Filter Graph 中的所有 Filter IEnumFilters *pEnum=NULL; HRESULT hr=pGraph->EnumFilters(&pEnum) ; if(SUCCEEDED(hr) ) { IBaseFilter pFilter=NULL; while(S_OK==pEnum->Next(1,&pFilter,NULL) ) { pGraph->RemoveFilter(pFilter);//删除 Filter pEnum->Reset() ;//复位枚举器内部状态 pFilter->Release() ; } pEnum->Release() ; } 递归调用,删除指定 Filter 下游的(连接着的)所有 Filter 的代码如下: void NukeDownstream(IGraphBuilder *inGraph,IBaseFilter *inFilter) {

    if(inGraph&&inFilter) { IEnumPinsb *pinEnum=0; if(SUCCEEDED(inFilter->EnumPins(&pinEnum) ) ) { pinEnum->Reset() ; IPin *pin=0; ULONG cFetched=0; bool pass=true; //枚举 Filter 上的所有 Pin while (pass&&SUCCEEDED (pinEnum->Next (1, &pin, &cFetched) ) ) { if (pin&&cFetched) { IPin *connectedPin = 0; //判断当前 Pin 是否处于连接状态 Pin->ConnectedTo(&connectedPin) ; if(connectedPin) { PIN_INFO pininfo; if ( SUCCEEDED ( connectedPin->QueryPinInfo (&pininfo) ) { //如果当前 Pin 连接着的 Pin 是输入 Pin,则说明我们能够得 //到一个下一级 Filter,我们要递归调用 NukeDownstream if(pininfo.dir==PINDIR INPUT) { //这里递归调用 NukeDownstream(inGraph,pininfo.pFilter) ; //当这一路下游 Filter 全部删除后,可以断开当 //前 Pin 连接 inGraph->Disconnect(connectedPin) ; inGraph->Disconnect(pin); //将操作完成的下一级 Filter 删除 inGraph->RemoveFilter(pininfo.pFilter); } Pininfo.pFilter->Release(); } connectedPin->Release(); } pin->Release(); } else {

    pass = false; } } pinEnum->Release(); } } } 递归调用,删除指定 Filter 上游的(连接着的)所有 Filter 的代码如下: void NukeUpstream(IGraphBuilder *inGraph, IBaseFilter *inFilter) { if(inGraph &&inFilter) { IEnumPins *pinEnum = 0; if(SUCCEEDED(inFilter->EnumPins(&pinEnum))) { pinEnum->Reset(): IPin *pin = 0; ULONG cFetched = 0; bool pass = true; //枚举 Filter 上的所有 Pin while(pass && SUCCEEDED(pinEnum->Next(1, &pin, &cFetched))) { if(pin && cFetched) { IPin *connectedPin = 0; //判断当前 Pin 是否处于连接状态 Pin->ConnectedTo(&connectedPin); if(connectedPin) { PIN_INFO pininfo; if(SUCCEEDED(connectedPin->QueryPinInfo(&pininfo))) { //如果当前 Pin 连接着的 Pin 是输出 Pin,则说明我们能够 //得到一个上一级 Filter,我们要递归调用 NukeUpstream if(pininfo.dir == PINDIR_OUTPUT) { //递归调用 NukeUpstream(inGraph,pininfo.pFilter) ; //当这一路上游 Filter 全部删除后, 可以断开当前 Pin //连接 inGraph->Disconnect(connectedPin) ; inGraph->Disconnect(pin) ; //将操作完成的上一级 Filter 删除 inGraph->RemoveFilter(pininfo.pFilter) ;

    } pininfo.pFilter->Release() ; } connectedPin->Release() ; } pin->Release() ; } else { pass=false; } } pinEnum->Release() ; } } } 第六章 视频采集 6.1 绑定采集设备为 Source Filter 视频程序要运行在不同的计算机,不同的操作系统上,但目标系统的硬件配 置情况是不确定的, 应用程序如何来使用这些不可预知的采集设备呢?答案是使 用系统枚举。 在 DirectShow 框架下,参与工作的基本单元是 Filter.采集设备也不例外。 DirectShow 使用特殊的包装 Filter 对它们进行包装—WDM 驱动模型的采集设备 使用 WDM Video Capture Filter(实现文件为 kswdmcap.ax) ,VFM 驱动模型的采 集设备使用 VFW Capture Filter(实现文件为 qcap.dll) 。只要采集设备正确安装, DirectShow 就能把它包装成一个 Filter,并且在一定的类型目录下注册。换句话 说,应用程序只要枚举特定的类型目录,就能在知道系统中安装有多少个,以及 何种类型的采集设备。 DirectShow 定义了很多类型目录供不同的 Filter 注册。音频采集设备注册在 Audio Capture Sources 目录下,视频采集设备注册在 Video Capture Sources 目录 下。而在上述两个目录下注册的采集设备一般会在 WDM Streaming Capture Devices 目录下注册。有时候,WDM Streaming Capture Devices 还会包含一些既 不在 Audio Capture Devices 目录下也不在 Video Capture Sources 下注册的采集设 备。类型目录都用一个 CLSID 来表示,表 6.1 给出了音视频采集设备 Filter 注册 的类型目录及其 CLSID 的对应关系。 表 6.1 音视频采集设备 Filter 的注册目录
    类型目录名
    Audio Capture Sources Video Capture Sources WDM Streaming Capture Devices

    类型目录的 CLSID
    CLSID_AudioInputDeviceCategory CLSID_VideoInputDeviceCategory AM_KSCATEGORY_CAPTURE

    类型目录的 MERIT
    MERIT_DO_NOT_USE MERIT_DO_NOT_USE MERIT_DO_NOT_USE

    DirectShow 提供了一个专门的系统枚举组件(CLSID_SystemDeviceEnum)。 枚举的大致过程如下: (1) 使 用 CoCreateInstance 函 数 创 建 一 个 系 统 枚 举 组 件 对 象 , 并 获 得

    ICreateDevEnum 接口。 (2) 使用接口方法 ICreateDevEnum::CreateClassEnumerator 为指定的类型目录 创建一个枚举器,并获得 IEnumMoniker 接口。 (3) 使用接口方法 IEnumMoniker::Next 枚举指定类型目录下所有的设备标志 (Device Moniker)。每个设备标志对象上都实现了 IMoniker 接口。 (4) 调用 IMoniker::BindToStorage 之后就可以访问设备标志的属性集, 比如得 到设备的显示名字(Display Name),友好名字(Friendly Name)等。 (5) 调用 IMoniker::BindToObject 可以将设备标志绑定成一个 DirectShow Filter,随后调用 IFilterGraph::AddFilter 加入到 Filter Graph 中就可以参与 工作了。 下面给出参考的函数: IBaseFilter* CreateDevice(BOOL inIsVideo, const char *inDisplayName, char *outFriendlyName) { GUIDguid=inIsVideo?CLSID_VideoInputDeviceCategory: CLSID_AudioInputDeviceCategory; return CreateHardwareFilter2(guid,inDisplayName,outFriendlyName); } IBaseFilter* CreateHardwareFilter2(GUID inCategory, const char *inDisplayName, char *outFriendlyName) { //创建一个系统枚举组件 ICreateDevEnum *enumHardware =NULL; HRESULT hr= CoCreateInstance( CLSID_SystemDeviceEnum, NULL, CLSCTX_ALL, IID_ICreateDevEnum, (void**)&enumHardware); if(hr){return NULL;} IBaseFilter *hardwareFilter =NULL; IEnumMoniker *enumMoniker =NULL;//为目录创建一个枚举器 hr=enumHardware->CreateClassEnumerator(inCategory,&enumMoniker,0); if(enumMoniker){ enumMoniker->Reset(); ULONG fetched=0; IMoniker *moniker=NULL; char displayName[1024];//枚举得到目录下所有硬件设备,逐个进行匹配 while(!hardwareFilter&&SUCCEEDED(fetched&&enumMoniker->Next(1,&monike r,&fetched))){ if(moniker){ WCHAR* wzDisplayName=NULL; //获得设备显示名字 hr=moniker->GetDisplayName(NULL,NULL,&wzDisplayName); if(SUCCEEDED(hr)){

    WideCharToMultiByte(CP_ACP,0,wzDisplayName,-1,displayName,1024,"", NULL); CoTaskMemFree(wzDisplayName); //判断显示名字是否匹配 if(IsSameDevice(displayName,inDisplayName)){ moniker->BindToObject(0,0,IID_IBaseFilter,(void**)&hardwareFilter);}} //如果成功创建 Filter,则获得该 Filter 的友好名字 if(outFriendlyName&&hardwareFilter){ IPropertyBag* propertyBag=NULL; outFriendlyName[0]=0; if(SUCCEEDED(moniker->BindToStorage(0,0,IID_IPropertyBag,(void* *)&propertyBag))){ VARIANT name; name.vt=VT_BSTR; if(SUCCEEDED(propertyBag->Read(L"FriendlyName",&name ,NULL))){ WideCharToMultiByte(CP_ACP,0,name.bstrVal,-1,outFriendlyN ame,1024,NULL,NULL);} propertyBag->Release();}} moniker->Release();}} enumHardware->Release();} return hardwareFilter; } 6.2 构建 Filter Graph 采集应用的 Filter Graph 一般比较复杂;而直接使用 Filter Graph Manager 上的 IGraphBuilder 接口构建这种 Filter Graph,有时候难度又很大。为此,DirectShow 特
    别提供了一个辅助组件 Capture Graph Builder,来简化这种 Filter Graph 的创建。 跟其他 DirectShow 应用程序一样, 在构建实际的 Filter Graph 之前, 肯定是要创建 Filter Graph Manager 组件的。另外,我们还可以创建一个 Capture Graph Builder 组件,对用这个 组件上的 ICaptureGraphBuilder2 接口来完成 Filter Graph 的构建,如图 6.2 所示:

    图 6.2 创建 Capture Graph Builder 组件 成功创建 Capture Graph Builder 组件之后,我们还必须对这个组件进行初始化,即调 用接口方法 ICaptureGraphBuilder2::SetFilter(pGraph)将 Filter Graph Manager 对象指针设置给 它。Capture Graph Builder 组件进行初始化的代码如下: HRESULT InitCaptureGraphBuilder(IGraphBuilder **ppGraph, ICaptureGraphBuilder2

    **ppBuild) { if(!ppGraph||!ppBuild){ return E_POINTER;} IGraphBuilder *pGraph =NULL; ICaptureGraphBuilder2 *pBuild=NULL; //创建 ICaptureGraphBuilder 组件 HRESULThr=CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)&pBuild); if(SUCCEEDED(hr)){ //创建 Filter Graph Builder hr=CoCreateInstance(CLSID_FilterGraph, 0, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)&pGraph); if(SUCCEEDED(hr)){//Initialize the Capture Graph Builder pBuild->SetFiltergraph(pGraph); //返回两个指针 *ppBuild=pBuild; *ppGraph=pGraph;//调用完后需释放 return S_OK; } else {pBuild->Release();}} return hr; } ▼使用 Caputre Graph Builder 组件构建 Filter Graph 的好处有: ▼使用 ICaptureGraphBuilder2::RenderStream 能够对指定类型的 Pin 完成后续连接(比如 PIN_CATEGORY_PREVIEW 指定 Preview Pin,PIN_CATEGORY_CAPTURE 指定 Capture Pin 等) ; ▼智能构建 Preview Pin 链路(指定 PIN_CATEGORY_PREVIEW 类型调用 RenderStream) , 即当采集卡 Filter 含有 VP Pin 时, 连接 Overlay Mixer2|Video Render;含有 Preview Pin 时, 连 接 Video Renderer;而只有 Capture Pin 时自动插入一个 Smart Tee Filter 进行分路后, 再连接到 Video Renderer; ▼自动加入采集卡 Filter 之前的辅助 Filter,如用于电视接收的 TV Tuner Filter,用于控制电视 声音的 TV Audio Filter 和用于选择视频输入端子的 Analog Video Crossbar Filter 等,并完成 这些 Filter 之间的连接(这些 Filter 之间的连接不是通过媒体类型来标识的,而是通过一种 媒介(Medium)的 GUID 来标志的,它们之间在 Filter Graph 中也不会有实际的数据流动) ; ▼ICaptureGraphBuilder2::FindPin 提供了方便的寻找 Pin 的方法; ▼ICaptureGraphBuilder2::FindInterface 提供了方便的寻找特定接口的方法; 使用 ICaputreGraphBuilder2::ControlStream 可以单独地控制某个 Pin(Preview Pin 或 Capture Pin);

    ▼自动处理 EC_REPAINT 事件(如果在采集写文件的时,我们同时使用传统的 Video Renderer 进行预览, 当视频窗口别别的窗口覆盖然后恢复时, Video Renderer 会向 Filter Graph Manager 发送一个 EC_REPAINT 事件要求重画视频窗口。Filter Graph Manager 的默认处理 是,对 Filter Graph 执行暂停操作,这将引起写到文件的内容异常。Caputre Graph Builder 会 自 动 帮 我 们 处 理 这 个 问 题 , 即 内 部 调 用 了 IMediaEvent::CancelDefaultHandling(EC_REPAINT). ICaptureGraphBuilder2 *pBuild; //Caputre Graph Builder //Initialize pBuilder (not shown) IBaseFilter *pCap; //Video capture filter /*Initialize pCap and add it to the filter graph (not shown)*/ Hr=pBuild->RenderStream(&PIN_CATEGORY_PREVIEW,&MEDIATYPE_Video,pCap,NULL, NULL); 比如,想要同时构建一个采集写文件的 Filter 链路,代码如下: IBaseFilter *pMux; Hr=pBuild->SetOutputFileName( &MEDIASUBTYPE_Avi,//Specifies AVI for the target file L”C:\\Example.avi”, //File name &pMux, //Receives a pointer to the mux NULL); //(Optional) Receive a pointer to the file sink Hr=pBuild->RenderStream( &PIN_CATEGORY_CAPTURE,//Pin category &MEDIATYPE_Video, //Media type pCap, //Capture filter NULL, //Intermediate filter (optional) pMux); //Mux or file sink filter //Release the mux filter pMux->Release(); 最终构建的 Filter Graph 如图 6.3 所示:

    图 6.3 采集写文件同时预览的 Filter Graph 6.3 添加 Filter 到 FilterGraph 前面我们已经提到根据指定的 CLSID 添加 Filter 到 Filter Graph 的普遍方法,但是并不 是所有的 Filter 都可以通过这个方法创建,我们提到使用枚举的方法添加 Filter 的方法,过 程与枚举系统硬件设备的过程相似,这里只给出程序,希望大家好好体会: IBaseFilter* ObtainFilterByFriendlyName(GUID inCategory, const char *inFriendlyName) { HRESULT hr; IBaseFilter *pFilter; CComBSTR FilterName(inFriendlyName);

    ICreateDevEnum *pSysDevEnum = NULL; hr = CoCreateInstance(CLSID_SystemDeviceEnum, CLSCTX_INPROC_SERVER,IID_ICreateDevEnum, (void **)&pSysDevEnum); IEnumMoniker *pEnumCat = NULL; hr = pSysDevEnum->CreateClassEnumerator(inCategory, &pEnumCat, 0); if (hr == S_OK) { // Enumerate the monikers. IMoniker *pMoniker; ULONG cFetched; while(pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK) { IPropertyBag *pPropBag; pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pPropBag); VARIANT varName; VariantInit(&varName); hr = pPropBag->Read(L"FriendlyName", &varName, 0); if ( (SUCCEEDED(hr) && wcscmp(FilterName, varName.bstrVal) == 0)) { pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter, (void**)&pFilter); return pFilter; } VariantClear(&varName); pPropBag->Release(); pMoniker->Release(); } pEnumCat->Release(); } pSysDevEnum->Release(); return ERROR; }

    NULL,
    展开全文
  • DirectShow获取视频和音频数据

    热门讨论 2013-03-23 09:46:02
    DirectShow获取视频和音频数据,对视频的易歌预览和截图,对音频的预览和保存到文件
  • directshow

    万次阅读 2010-03-05 18:42:00
    以文本方式查看主题 - 温馨小筑 (http://www.learnsky.com/bbs/index.asp) -- 电脑编程 (http://www.learnsky.com/bbs/list.asp?boardid=6) ---- DirectShow (http://www.learnsky.com/bbs/dispbbs.asp?

    以文本方式查看主题
    温馨小筑   (http://www.learnsky.com/bbs/index.asp)
    --  电脑编程   (http://www.learnsky.com/bbs/list.asp?boardid=6)
    ----  DirectShow   (http://www.learnsky.com/bbs/dispbbs.asp?boardid=6&id=868)


    --  作者:admin
    --  发布时间:2005-11-26 2:46:00
    --  DirectShow

        最近一段时间,在编写DirectShow应用程序时常常遇到一些问题,原因是对DirectShow技术没有较全面地掌握,对各个接口间的关系以及filter与filter之间连接的内部过程等都只是一知半解,除了再仔细地看看DirectShow的基类库源文件之外,觉得也很有必要从头到尾看一遍DirectShow的MSDN文档。在看时顺便有选择地翻译出来,一来以便以后再看时可以轻松点,二来也敦促自己不能不求甚解早早看看了事。在翻译的过程中也加了一些自己的补充,因为觉得某些MSDN章节实在是过于简单还有些模棱两可。

    1. DirectShow介绍
        DirectShow是一个 windows平台上的流媒体框架,提供了高质量的多媒体流采集和回放功能。它支持多种多样的媒体文件格式,包括ASF、MPEG、AVI、MP3和 WAV文件,同时支持使用WDM驱动或早期的VFW驱动来进行多媒体流的采集。DirectShow整合了其它的DirectX技术,能自动地侦测并使用可利用的音视频硬件加速,也能支持没有硬件加速的系统。
        DirectShow大大简化了媒体回放、格式转换和采集工作。但与此同时,它也为用户自定义的解决方案提供了底层流控制框架,从而使用户可以自行创建支持新的文件格式或其它用途的DirectShow组件。
        以下是几个使用DirectShow编写的典型应用:DVD播放器、视频编辑应用、AVI到ASF转换器、MP3播放器和数字视频采集应用。
        DirectShow 是建立在组件对象模型(COM)上的,因此当你编写DirectShow应用时,你必须具备COM客户端程序编写的知识。对于大部分的应用,你不需要实现自己的COM对象,DirectShow提供了大部分你需要的DirectShow组件,但是假如你需要编写自己的DirectShow组件,你还需要具备编写COM组件的知识。
    1.1. DirectShow支持的格式
        DirectShow是一个开放的框架,因此只要有合适的filter来分析和解码,它可以支持任何格式。DirectShow默认支持以下的文件类型和压缩格式:
        注:打*号的需要Windows Media Format SDK支持
        文件类型:
          Windows Media? Audio (WMA)*
          Windows Media? Video (WMV)*
          Advanced Systems Format (ASF)*
          Motion Picture Experts Group (MPEG)
          Audio-Video Interleaved (AVI)
          QuickTime (version 2 and lower)
          WAV
          AIFF
          AU
          SND
          MIDI
        压缩格式:
          Windows Media Video*
          ISO MPEG-4 video version 1.0*
          Microsoft MPEG-4 version 3*
          Sipro Labs ACELP*
          Windows Media Audio*
          MPEG Audio Layer-3 (MP3) (decompression only)
          Digital Video (DV)
          MPEG-1 (decompression only)
          MJPEG
          Cinepak
        微软自己没有提供MPEG2解码器,一些可用的DirectShow MPEG2硬件或软件解码器是由第三方提供的。
    1.2. 常见问题集(摘录)
    1.2.1. 一般问题

    *DirectShow支持哪些操作系统?
          DirectShow 支持Windows9X、Windows2000、Windows Me和Windows XP。
    *使用 DirectShow需要多少COM知识?
          应用程序开发者只需要基本的COM组件知识:实例化COM组件、调用接口、管理接口的引用计数。Filter开发者则需要更多。
    *有与DirectShow兼容的硬件列表(HCL)吗?
          没有。如果硬件兼容DirectShow,DirectShow会使用它们,如果没有兼容的硬件,DirectShow使用GDI绘制视频,以及使用 WaveOut系列多媒体API来播放音频。
    *可以使用哪些语言来编写DirectShow应用?
          DirectShow 主要为C/C++开发设计。Visual Basic只能使用其中的很小一部分。可以通过MS JScript或VB Script来支持基于脚本的DVD和TV应用。也可能用Delphi来编写,但SDK文档不提供这方面的内容。
    *DirectShow 会通过托管代码实现吗?
          目前还没有这个计划。DirectX SDK提供了有限的使用音视频回放类的托管回放功能,你可以使用COM interop创建托管代码的DirectShow客户端应用,但是因为性能上的原因,不推荐创建运行在CLR上的filter。

    *DirectShow开发需要什么样的编译器
          任何能够产生COM对象的编译器都可以。
    *DirectShow和DirectX的其它组件的关系
          DirectShow 和DirectX的其它组件在内部进行联系。DirectShow在硬件的支持下使用DirectSound和DirectDraw。Video Renderer和Overlay Mixer使用DirectDraw 3和DirectDraw5表面(surfaces)。Video Mixing Renderer 7(只支持WINXP)使用DirectDraw7表面。Video Mixing Renderer 9使用最新的(目前是Directx9)Direct3D API函数。即便是某个应用程序包含了DirectX其它组件,你也不必使用其它组件的API去编写它。参考SDK的例子:Texture3D Sample。
    *DirectShow与ActiveMovie的关系?
          ActiveMovie 是DirectShow原来的名称,现已不再使用,但是一部分API仍保留了"AM"的前缀,比如AM_MEDIA_TYPE和 IAMVideoAccelerator。
    *DirectShow是限于多媒体应用吗?
          DirectShow 默认包含的组件主要是为音视频流设计的,但是,DirectShow框架已经成功地用于其它数据流的解决方案中。
    *GraphEdit 工具有源码吗?GraphEdit.exe是否可再发布?
          没有源码,不可再发布。
    *DMO 可以代替DirectShow filter吗?
          在编写编码器、解码器、效果器应用时,鼓励用DMO代替 DirectShow filter。在其它的应用中,使用DirectShow filter可能会比较合适。
    1.2.2. 程序编写问题
    *如何设置编译环境,需要哪些头文件和库?
          参考"设置编译环境"章节
    *GraphEdit列示了很多没有文档支持的filter,它们都是些什么?
          GraphEdit 枚举了所有作为filter类型注册在系统中的filter,包括由第三方应用程序安装的filter,以及其它微软技术如Windows Media或NetMeeting安装的,另外,一些DirectShow filter被用来做硬编码或硬解码驱动的外壳。Microsoft H.263 Video Codec用于NetMeeting,不再被DirectShow支持。
    *如何知道 DirectShow已经被安装?
          调用CoCreateInstance创建一个Filter Graph Manager实例,如果成功,表示DirectShow已经被安装,下面是一个例子:

          IGraphBuilder *pGraph;

           HRESULT hr = CoCreateInstance(CLSID_FilterGraph,
                 NULL, CLSCTX_INPROC_SERVER,
                 IID_IGraphBuilder, (void **) &pGraph);

    *如果不通过属性设置页来更改filter的设置?
          当然是通过filter提供的接口罗。如果没有提供,就没有办法啦
    *DirectShow能通知应用程序当前回放位置吗?
          不提供回调来通知位置,需要使用一个计时器定时调用 IMediaSeeking::GetCurrentPosition方法来得到当前回放位置。
    *filter 运行在哪个特权级别下?
          运行在Ring 3特权级别下,某些流控制驱动(如音视频采集驱动)运行在Ring 0特权级别下。
    *需要一个Kernel调试器吗?
          这依据具体的项目。安装DirectX调试运行时库(DirectX debug runtime library)意味着安装调试驱动(Debug driver)和其它核心组件(kernel mode component),因此如果你的应用程序在其中的某个组件中产生了一个调试断言(debug assert),你的机器就会自动重启除非你拥有一个kernal调试器。
    *DEFINE_GUID宏是怎么工作的?
          使用DEFINE_GUID宏可以让你通过包含同一个头文件来定义GUID值而不必使用extern关键词。比如,你的工程中有三个源文件:src1.cpp,src2.cpp,src3.cpp,它们都使用一个相同的GUID值,而为了保证一致性,这个GUID只能在你的工程中定义一次,这时,其它的源文件必须定义外部引用来使用它。用了DEFINE_GUID,你可以使用在所有源文件中包含同一个头文件,在头文件中这样定义GUID:

        DEFINE_GUID(CLSID_MyObject,
             0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);

                这个例子中GUID为0,实际编程中请用Guidgen工具来产生一个GUID,在其中的一个源文件中,在你的头文件前包含initguid.h,如:

      // Src1.cpp
    #include
    #include "MyGuids.h"

    // Src2.cpp
    #include "MyGuids.h"

    // Src3.cpp
    #include "MyGuids.h"

    在没有包含Initguid.h的地方,DEFINE_GUID宏创建外部引用来使用GUID值,在包含Initguid.h的地方,DEFINE_GUID重定义DEFINE_GUID宏以产生GUID的定义。
    如是没有在任何地方添加 Initguid.h,你会得到一个链接错误:"unresolved external symbol." ,如果同样的GUID包含Initguid.h两次,会得到编译错误"redefinition; multiple initialization."要解决这些问题,请确认Initguid.h只包含一次。同样的,不要包含Initguid.h到预编译头文件中去,因为预编译头文件会被每个源文件包含。

    [此贴子已经被作者于 2005-11-26 2:49:14编辑过]


    --  作者:admin
    --  发布时间:2005-11-26 2:46:00
    --  
    桃花坞里桃花庵,桃花庵下桃花仙。
    桃花仙人种桃树,又摘桃花换酒钱。
    酒醒只在花前坐,酒醉还来花下眠。
    半醉半醒日复日,花落花开年复年。
    但愿老死花酒间,不愿鞠躬车马前。
    车尘马足显者事,酒盏花枝隐士缘。
    若将显者比隐士,一在平地一在天。
    若将花酒比车马,彼何碌碌我何闲。
    别人笑我太疯癫,我笑他人看不穿。
    不见五陵豪杰墓,无花无酒锄作田。


    --  作者:admin
    --  发布时间:2005-11-26 2:48:00
    --  开始DirectShow旅程

    2. 开始DirectShow旅程
        这个章节的内容主要是编写 DirectShow应用所需的一些基本概念,可以把它当作一个高级介绍,理解这些内容只需具备一般的编程和有关多媒体的知识。
    2.1. 设置DirectShow开发的编译环境
        这节内容描述了如何来编译DirectShow应用。你可以使用命令行形式来编译一个工程,也可以在Microsoft Visual Studio集成环境下(包含VC++)实现。
        头文件:
        所有的DirectShow应用都需要Dshow.h这个头文件,某些DirectShow接口需要附加的头文件,参考接口的说明视具体情况定。
        库文件:
        DirectShow使用以下库文件:
        Strmiids.lib 输出类标识(CLSID)和接口标识(IID),所有DirectShow应用均需此库。
        Quartz.lib   输出AMGetErrorText函数,如果不调用此函数,此库不是必需的。
        有了以上这些头文件和库文件,你已经可以编写 DirectShow应用了,但是微软建议使用DirectShow基类库来编写filter,这样可以大大减少程序编写的工作量。要使用 DirectShow基类库,需要先编译它,基类库位于SDK的Samples//Multimedia//DirectShow //BaseClasses文件夹下,包含两个版本的库:发布版(retail version)Strmbase.lib和调试版(debug version)Strmbasd.lib。具体参见"创建DirectShow Filter"一节。
    2.2. DirectShow应用程序编程简介
        这节介绍DirectShow用到的一些基本术语和概念,看完这节后,你将能够编写你的第一个DirectShow应用程序。
    Filter和Filter Graph
        一个DirectShow应用程序是由一个个称为filter 的软件构件组合而成的,filter执行一些多媒体流的操作,如:读文件、从视频采集设备中获得视频、将不同的格式的流解码如MPEG1、将数据送到图形卡或声卡中去。
        Filter 接收输入并产生输出。举个例子,一个解码MPEG1视频流的filter,输入MPEG1格式的视频流,输出一系列未压缩的视频帧。
        在 DirectShow中,应用程序要实现功能就必须将这些filter链接在一起,因而一个filter的输出就变成了另一个filter的输入。这一系列串在一起的filter称为filter graph 。例如,下图就显示了一个播放avi文件的 filter graph:

        File Source(Async) filter从硬盘中读取avi文件;AVI Splitter filter分析文件并将其分解成两个流:一个压缩的视频流和一个音频流;AVI Decompressor filter将视频帧解码,Video Renderer filter将解码后的视频帧通过DirectDraw或GDI显示出来;Default DirectSound Device filter使用DirectSound播放音频流。
        应用程序没有必要对这些数据流进行管理,而是通过一个叫Filter Graph Manager这个上层组件来控制这些filter。应用程序调用上层API如"Run"(通过graph移动数据)或"Stop"(停止移动数据)。如果你需要对数据流作更多的操作,你可以通过COM接口直接进入filter。Filter Graph Manager同样也输出事件通知给应用程序。
        Filter Graph的另一个用途是将filter连在一起创建一个filter graph。
        编写一个DirectShow应用程序大体需要三个步骤:
        1.创建一个Filter Graph Manager的实例
        2.使用Filter Graph Manager创建一个filter graph,此时,需要已经具备所有必需的filter。
        3.使用Filter Graph Manager控制filter graph和通过这些filter的流,在这个过程中,应用程序会收到Filter Graph Manager发送的事件。
        完成这些后,应用程序需发布这个Filter Graph Manager和所有的filter。
    2.3. 播放一个文件
        这一章以本节这个有趣的例子来结束,这个例子是一个播放音频或视频文件的简单控制台程序。程序只有寥寥数行,但却展示了DirectShow编程的强大能力。
        正如上一节所讲的创建DirectShow应用程序的三个步骤,第一步,首先,需要调用CoInitialize来作初始化,然后调用CoCreateInstance创建Filter Graph Manager:

        HRESULT hr = CoInitialize(NULL);
        if (FAILED(hr))
        {
            return;
        }

        IGraphBuilder *pGraph;
        HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL,
            CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&pGraph);

        如上所示,类标识符(CLSID)是CLSID_FilterGraph。Filter Graph Manager由进程内DLL(in-process DLL)提供,因此参数3,dwClsContext的值为CLSCTX_INPROC_SERVER。由于DirectShow运行自由线程模式 (free-threading model),所以你同样可以使用COINIT_MULTITHREADED参数来调用CoInitializeEx。
        第二步是创建filter graph,调用CoCreateInstance得到的IGraphBuilder接口包含了大部分创建filter graph的方法。在这个例子中还需要另外两个接口:IMediaControl和IMediaEvent。
        IMediaControl 控制数据流,它包含开启和停止graph的方法;IMediaEvent包含从Filter Graph Manager获取事件的方法,在这个例子中,这个接口用来得到回放结束事件。
        所有这些接口由Filter Graph Manager提供,使用得到的IGraphBuiler接口指针来查询得到。

        IMediaControl *pControl;
        IMediaEvent   *pEvent;
        hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
        hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

        现在你可以创建filter graph了,对于文件回放只需要一个简单的调用:

       hr = pGraph->RenderFile(L"C:Example.avi", NULL);

        IGraphBuilder::RenderFile方法创建了一个能够播放指定文件的 filter graph,事实上,原本需要做的一些如创建filter实例及将这些filter连接起来的工作,都由这个方法自动完成了,如果是视频文件,这个 filter graph看起来应该是这个样子:
        [file source]->[如果是缩格式,这里是个解码器]->[Video Renderer]
        要开始回放,调用 IMediaControl::Run方法:

          hr = pControl->Run();

        当filter graph运行时,数据经过各个filter最后回放为视频或音频。回放发生在一个单独的线程中。你可以通过调用 IMediaEvent::WaitForCompletion方法来等待回放的结束:

        long evCode = 0;
        pEvent->WaitForCompletion(INFINITE, &evCode);

        这个方法在播放期间被阻塞,直至播放结束或超时。
        当应用程序结束时,需要释放接口指针并关闭COM库:

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

        // Build the graph. IMPORTANT: Change this string to a file on your system.
        hr = pGraph->RenderFile(L"C:Example.avi", NULL);
        if (SUCCEEDED(hr))
        {
            // Run the graph.
            hr = pControl->Run();
            if (SUCCEEDED(hr))
            {
                // Wait for completion.
                long evCode;
                pEvent->WaitForCompletion(INFINITE, &evCode);

                // Note: Do not use INFINITE in a real application, because it
                // can block indefinitely.
            }
        }
        pControl->Release();
        pEvent->Release();
        pGraph->Release();
        CoUninitialize();
    }


    --  作者:admin
    --  发布时间:2005-11-26 2:49:00
    --  3. 关于DirectShow

    3.1. DirectShow体系概述
        多媒体的难题
        处理多媒体有几个主要的难题:
        *多媒体流包含了巨大的数据量,而这些数据都必须非常快地被处理
        *音频和视频必须同步,因此它们必须在同一时间开始或停止,并以同一速率播放
        *数据可能来自很多的源,如本地文件、网络、电视广播和视频摄像机
        *数据有各种各样的格式,如 AVI、ASF、MPEG和DV
        *程序员无法预知最终用户使用什么样的硬件设备
        DirectShow的解决方案
        DirectShow 被设计成用来解决所有这些难题,它主要的设计目的就是通过将复杂的数据转输、硬件的多样性和同步问题从应用程序中独立出来,从而简化在windows平台上数字媒体应用程序的开发任务。
        要实现数据高效地被处理,需要流化音视频数据,而DirectShow会尽可能地使用 DirectDraw和DirectSound,从而高效地将数据送到用户的声音和图形设备中进行播放。同步则是通过在媒体数据中加入时间戳来实现。而 DirectShow模块化的架构,使其可以轻松操纵变化多端的源、格式和硬件设备,在这样的架构里,应用程序只需组合和匹配多个filter来实现功能。
        DirectShow提供的filter支持基于WDM的采集和调谐设备,也支持早先的VFW采集卡和为ACM和VCM接口编写的编码器。
        下图显示了应用程序、DirectShow组件和DirectShow支持的硬件和软件组件之间的关系:

        如图,DirectShow将应用程序与众多复杂的设备隔离开来,通信和控制这些设备均出DirectShow的 filter来完成。DirectShow同样为某种文件格式提供与之对应的编解码器。


    --  作者:admin
    --  发布时间:2005-11-26 2:50:00
    -- 

    3.2. Filter Graph和它的组件
        这一节描述了 DirectShow的主要组件,为DirectShow应用程序和DirectShow Filter开发者提供一个介绍。应用程序开发者可以忽略掉很多底层部分,但是,了解底层对于理解DirectShow架构还是很有帮助的。
    3.2.1. 关于DirectShow Filter
        DirectShow使用一个模块化的架构,每个处理过程都由一个叫做filter 的COM对象来实现。DirectShow为应用程序提供了一系列标准的filter,开发者也可以编写自己的filter来扩展DirectShow的功能。下面是播放一个AVI文件的各个步骤:
        *从文件中读取数据并转成字节流(File Source filter)
        *检查AVI头,分析字节流并将它们分离成视频和音频(AVI Aplitter filter)
        *将视频解码(不同的解码filter,取决于不同的压缩格式)
        *将视频显示出来(Video Renderer filter)
        *将音频送入声卡(Default DirectSound Device filter)

        如图所示,每个filter与一个或多个其它的filter相连,其中的连接点也是一个COM对象,称作Pin ,filter使用Pin将数据从一个filter转移到另一个,图中的箭头指示了数据流动的方向。在DirectShow中,这一系列连接在一起的filter称作filter graph。
        Filter可能处于有三种不同的状态:运行、停止和暂停状态。filter在运行状态时处理数据,停止状态时停止处理数据,暂停状态则是表示就绪,可以开始进入运行状态。除了极个别的情况,一个filter Graph中的所有filter通常都处理同一个状态下,因此,filter graph也可以称其处于运行、停止、暂停状态。
        Filter 可以被分成几个大的种类:
    *source filter - filter graph的数据源,这些数据可以来自文件、网络、摄像头或任何其它东西。每一个source filter操纵不同类型的数据源。
    *transform filter - 接收数据,处理数据并将它送入下一个filter。编码filter和解码filter都属于这个种类。
    *Renderer filter - 处于filter链的未端,接受数据并将其展现给用户。比如,一个视频renderer在显示器上绘制视频图像;一个音频renderer将音频数据送入声卡;一个写文件filter(file-writer filter)将数据存盘。
    *splitter filter - 分析输入的数据流并将其分解成两路或多路,比如,AVI splitter分析字节流并将其分解成视频流和音频流。
    *mux filter - 将多路输入流合并成一路。比如,AVI Mux正好与AVI splitter做相反的工作,它将视频和音频流合成为一个AVI格式的字节流。
        以上的分类并不是绝对的,比如,ASF Reader Filter同时充当了source filter和splitter filter的角色。
        所有的DirectShow filter都提供IBaseFilter接口,所有的Pin也都提供IPin接口。DirectShow也定义了许多其它的接口以实现特定的功能。

    3.2.2. 关于Filter Graph Manager
    Filter Graph Manager 是一个用以控制 filter graph中的filter的COM对象。它提供了许多功能,包括:
        *协调filter之间的状态变化
        *建立参考时钟(reference clock)
        *将事件返回给应用程序
        *提供应用程序建立filter graph的方法
        这里先简单地描述一个这些功能。
    状态变化: filter们的状态变化必须遵照一个特定的次序,因此,应用程序不能将状态变化的命令直接发给filter,而是将一个简单的命令发给filter graph manager,由它来将命令分发给各个filter。定位命令同样使用这种方式,应用程序发送一个定位命令给filter graph manager,由它来分发。
    参考时钟: 在filter graph中的所有filter都使用一个相同的时钟,称为参考时钟(reference clock)。参考时钟保证了所有流的同步。一个视频帧或一段音频样本被播放的时间钞称作呈现时间(presentation time)。呈现时间精确地相对于参考时钟。Filter Graph Manager通常选择的参考时钟是声卡参考时钟或系统时钟。
    Graph 事件: filter graph manager使用一个消息队列来通知应用程序发生在filter graph中的事件。
    Graph-buliding 方法: filter graph manager提供给应用程序将filter加入到filter graph中的方法,以及将filter与filter连接或断开连接的方法。
        Filter graph manager不提供操纵在filter之间流动数据的功能,这个功能由filter通过pin连接在一个单独的线程中自行完成。
    3.2.3. 关于媒体类型(Media Type)
        因为DirectShow是模块化的,因此需要有一个在filter graph各个点之间描述格式的方法。比如说,AVI回放,数据输入时是一个RIFF块的流,然后被分解成视频和音频流。视频流由一个个可能被压缩的视频帧组成,解压后,视频流又变成了一系列未压缩的位图。音频与视频类似。
    Media Type:DirectShow怎样来描述格式     Media Type是描述数字媒体格式的常用方式。当两个filter连接时,它们需要协商决定同一个Media Type。Media Type标识了从上一个filter递交到下一个filter或物理层的数据流格式。如果两个filter对Media Type不能协商一致,则不能连接。
        对于某些应用程序,你不必去关心Media type,比如文件回放,DirectShow做了所有有关它的事情。
        Media type使用AM_MEDIA_TYPE结构体来定义,这个结构体包含了以下内容:
    *Major type: 主类型,是一个GUID,定义了数据的整体类型,包括了:视频、音频、未分析的字节流、MIDI等。
    *Subtype: 子类型,另一个GUID,进一步定义了数据格式。比如,如果主类型是视频,则子类型可以是RGB-24、RGB-32、UYVY等格式,如果主类型是音频,则可能是PCM或MPEG-1 payload等。子类型提供了比主类型更多的内容,但仍未提供完整的格式定义,比如,子类型没有定义图像尺寸和帧率,这些都将在Format block中被定义。
    *Format block: 格式块,定义了具体的格式。格式块是 AM_MEDIA_TYPE结构体中一个单独被分配的内存空间,pbFormat成员指向这块内存空间。因为不同的格式会有不同的格式描述,所以 pbFormat成员的类型是void*。比如,PCM音频使用WAVEFORMATEX结构体,视频使用不同的结构体包括:VIDEOINFOHEADER和VIDEOINFOHEADER2。formattype成员是一个GUID,指定了格式块包含了哪种结构体,每一种格式的结构体都被分配了GUID。cbFormat成员定义了格式式块的长度。
        当格式块被定义时,主类型和子类型包含的信息就显得有点多余了。其实,主类型和子类型为识别格式提供了一个便利的方法,比方说,你可以指定一个普通的24位RGB格式(MEDIASUBTYPE_RGB24),而不需去关心VIDEOINFOHEADER结构体中诸如图像尺寸和帧率这些信息。
        下面是一个filter检查媒体类型的例子:

    HRESULT CheckMediaType(AM_MEDIA_TYPE *pmt)
    {
        if (pmt == NULL) return E_POINTER;

        // 检查主类型,我们需要的是视频
        if (pmt->majortype != MEDIATYPE_Video)
        {
            return VFW_E_INVALIDMEDIATYPE;
        }

        // 检查子类型,我们需要的是24-bit RGB.
        if (pmt->subtype != MEDIASUBTYPE_RGB24)
        {
            return VFW_E_INVALIDMEDIATYPE;
        }

        // 检查format type和格式块的大小.
        if ((pmt->formattype == FORMAT_VideoInfo) &&
             (pmt->cbFormat >= sizeof(VIDEOINFOHEADER) &&
             (pmt->pbFormat != NULL))
        {
            // 现在可以安全地将格式块指针指向正确的结构体。
            VIDEOINFOHEADER *pVIH = (VIDEOINFOHEADER*)pmt->pbFormat;
            // 检查pVIH (未展示). 如果正确,返回S_OK.
            return S_OK;
        }

        return VFW_E_INVALIDMEDIATYPE;
    }

        AM_MEDIA_TYPE结构体还包含了一些任选项,用来提供附加的信息,filter不需要这些信息:
    *ISampleSize, 如果这个字段非零,表示这是每个sample的尺寸,如果是零,则表示sample的尺寸会改变。
    *bFixdSizeSamples ,如果这个布尔类型的标记是TRUE,表示ISampleSize有效,否则,你可以忽略ISampleSize。
    *bTemporalCompression ,如果这个布尔类型的标记是FALSE,表示所有帧都是关键帧。

    3.2.4. 关于媒体样本(Media Sample)和分配器(Allocator)
        Filter 通过Pin与Pin之间的连接来递交数据,数据从一个filter的输出Pin转移到另一个filter的输入Pin,除了个别情况,实现这种功能通常的方法是调用输入Pin上的IMemInputPin::Receive方法。
        依靠filter,媒体数据的内存空间可以通过多个途径来分配:在堆上、在DirectDraw表面(surface)、在共享GDI内存或使用其它的分配机制。这个负责分配内存空间的对象称为分配器(Allocator),是一个暴露 IMemAllocator接口的COM对象。
        当两个Pin相连时,其中的一个Pin必须提供一个分配器。DirectShow定义了一个方法调用序列来决定到底由哪个Pin来提供分配器。Pin还负责协商分配器创建的缓冲数和每个缓冲的尺寸。
        在数据流开始之前,分配器创建了一个缓冲池。在数据流动过程中,上游filter在缓冲中填入数据并递送给下游filter,但是,上游filter递送给下游filter的并不是原始的缓冲区指针,而是一个称为媒体样本(Media Sample)的COM对象,它由分配器创建并用来管理缓冲区,暴露IMediaSample接口。一个媒体样本包含:
        *指向下层缓冲区的指针
        *时间戳
        *各种标记
        *可选的媒体类型
        时间戳定义了呈现时间(presentation time),用以让renderer filter确定播放的合适时机。各种标记可以用来指示很多事情,比如,数据在上一个sample后是否被打段过(如重新定位、掉帧)等。媒体类型为流中间改变数据格式提供了途径,通常,没有媒体类型的sample,被认为从上一个sample以来数据格式没有被改变过。
        当filter使用一个缓冲时,它保存了sample上的参考计数。分配器使用参考计数来决定什么时候可以重用这个缓冲,这防止了一个filter在写一个缓冲时另一个 filter还在使用这个缓冲,除非所有的filter都释放了这个缓冲,否则sample不会将其返回给分配器的缓冲池。
    3.2.5. 硬件如何参与Filter Graph
        这一节描述了DirectShow如何与音频和视频硬件交互。
    外壳filter(Wrapper Filter)
        所有的DirectShow filter都是用户模式的软件组件。为了使象视频采集卡这样的内核模式的硬件驱动加入到filter graph中,必须使其象用户模式的filter那样。DirectShow提供外壳filter来完成这个功能,这类filter包括:Audio Capture filter、VFW Capture filter、TV Tuner filter、TV Audio filter和Analog Video Crossbar filter。DirectShow也提供一个叫KsProxy的filter,它可以实现任何类型的WDM流驱动。硬件商通过提供一个Ksproxy plug-in来扩展KsProxy,以使其支持自己的功能,ksproxy plug-in是一个被KsProxy聚合的COM对象。
        外壳filter通过暴露COM接口来实现设备的功能。应用程序使用这些接口将信息传递给filter,filter再把这些COM调用转化为设备驱动调用,将信息传递到内核模式下的设备中去,然后返回结果给应用程序。TV Tuner、TV Audio、Analog Video Crossbar和KsProxy filter都通过IKsPropertySet接口来支持驱动的自定义属性,VFW Capture filter和Audio Capture filter不支持这种方式。
        外壳filter使应用程序可以象控制其它 directshow filter一样来控制设备,filter已经封装了与内核驱动通信的细节。
    Video for Windows Devices
        VFW Capture filter支持早期的VFW采集卡,当一个设备加入到目标系统中支后,它可以被directshow使用系统设备枚举器(System Device Enumerator)发现并加入到filter graph中去。
        音频采集(Audio Capture)和混音设备(声卡)(Mixing Device/Sound Card)
        较新的声卡都有麦克风等设备的插口,而且大多数这类声卡都有板级的混频能力,可单独控制每一个连接设备的音量及高低音。在directshow中,声卡的输入和混频设备被Audio Capture filter封装。每个声卡都能被系统设备枚举器发现。要查看你的系统中的所有声卡,只需打开GraphEdit,从Audio Capture Sources一类中选择即可,每个在这个类里的filter都是一个单独的Audio Capture filter。

    WDM流设备     较新的硬解码设备和采集卡都遵照WDM规范。这些设备和比VFW设备更强大的功能,以及可以应用于多种系统(winxp,winNT,win2000,win98/me)。WDM视频采集卡支持许多VFW所没有的功能,包括枚举采集的格式、编程控制视频参数(如对比度、亮度)、编程选择输入端和电视调谐支持。
        为了支持WDM流设备,directshow提供了KsProxy filter(ksproxy.ax)。KsProxy被称为“瑞士军刀",因为它可以做很多不同的事情。filter上pin的数量,以及COM接口的数量,取决于底层驱动的能力。KsProxy不以"KsProxy"这个名字显示在filter graph中,而是使用一个已在注册表中登记的设备名称。要查看你系统中的WDM设备,可以运行GraphEdit然后从WDM Streaming这个类别中选择。即使你的系统中只有一块WDM卡,这块卡也可能包含多个设备,而每一个设备都表现为一个filter,每个 filter是实际意义上的KsProxy。
        应用程序使用系统设备枚举器在系统中寻找WDM设备moniker,然后调用moniker 的BindToObject来实例化。因为KsProxy能够表现所有类型的WDM设备,因此它必须通过询问驱动来决定哪些属性是驱动所支持的。属性集是一组数据结构的集合,被WDM设备使用,也被诸如MPEG2软解码filter这样的用户模式filter使用。KsProxy通过暴露COM接口来配置自己,硬件商则通过提供插件来扩展KsProxy,插件暴露硬件商自定义的一些接口,用以实现特殊的功能。所有这些细节对于应用程序来说都是不可见的,应用程序通过KsProxy控制设备就象控制其它的DirectShow filter一样。
    内核流
        WDM设备支持内核流,在内核流中数据在内核模式下被彻底流化而永远不需要切换到用户模式下去,从而避免了在内核模式和用户模式之间切换的巨大开销,内核流允许高的比特率而不消耗CPU的时间。基于 WDM的filter能够使用内核流将多媒体数据一个硬件设备送入到另一个中去,既可以是在同一块卡中也可以在不同的卡中,而不需要将数据拷入系统主存。
        从应用程序的视点来看,数据好象是从一个用户模式的filter传到另一个中去,但是实际上,数据根本就没有传到用户模式下过,而是可能支接从内核模式的设备中传到下一个中去直至被呈现(render)在显卡上。某些情况,比如采集视频到一个文件中去,在某些点上需要将数据从内核模式传入到用户模式,但是,仍然没有必要将数据拷贝到内存的一个新位置中去。
        应用程序开发者通常只需了解一个内核流的背景知识而不需要深究它的细节。


    --  作者:admin
    --  发布时间:2005-11-26 2:51:00
    -- 

    3.3. 构建Filter Graph
    3.3.1. 用于构建Graph的组件

        DirectShow 提供了一系列用于构建filter graph的组件,包括:
    *Filter Graph Manager 。这个对象用于控制filter graph,支持IGraphBuilder、IMediaControl和IMediaEventEx等许多接口。所有的directshow应用程序都需要在某些地方用到这个对象,虽然在有些情况下,是其它的对象为应用程序创建了filter graph manager。
    *Capture Graph Builder 。这个对象为构建filter graph提供附加的方法。它最初是为构建提供视频采集的graph而设计的(这正是它的名字由来),但是对于构建许多另外类型的filter graph也是很有用的。它支持ICaptureGraphBuilder2接口。
    *Filter Mapper和System Device Enumerator 。这些对象用于查找在系统中注册的或代表硬件驱动的filter。
    *DVD Graph Builder 。这个对象构建用以回放和导航DVD的filter graph。它支持IDvdGraphBuilder接口。基于脚本的应用程序能够使用MSWebDVD ActiveX控件来控制DVD回放。
    *Video Control 。WinXP提供这个ActiveX控件,用于操纵directshow中的数据和模拟电视。
    智能连接(Intelligent Connect)
        智能连接这个术语覆盖了一系列Filter Graph Manager用于构建所有或部份filter graph的算法。任何时候,当Filter Graph Manager需要添加filter来完成graph时,它大致做以下几件事情:
        1.如果有一个filter存在于 graph中,而且这个filter有至少一个没有连接的input pin,Filter Graph Manager试着去试用这个filter。
        2. 否则,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组合。

    3.3.2 Grap构建概述
        创建一个filter graph,从创建一个Filter Graph Manager实例开始:

         IGraphBuilder* pIGB;
         HRESULT hr = CoCreateInstance(CLSID_FilterGraph,
         NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder,(void **)&pIGB);

    Filter Graph Manager支持下列Graph构建方法:
    *IFilterGraph::ConnectDirect ,在两个pin之间进行直接连接,如果连接失败,则返回失败
    *IFilterGraph::Connect ,连接两个Pin,如果可能的话,直接连接它们,否则,在中间加入其它的filter来完成连接。
    *IGraphBuilder::Render ,从某个输出Pin处开始完成余下的graph构建。该方法会自动在输出pin后面添加必须的filter,直到renderer filter为止。
    *IGraphBuilder::RenderFile ,构建一个完整的文件回放graph。
    *IGraphBuilder::AddFilter ,将一个 filter添加到graph中。它不连接filter,并且在调用此方法前,filter必须已经被创建。创建filter可以是用 CoCreateInstance方法或使用Filter Mapper或系统设备枚举器(System Device Enumerator)。
        这些方法提供了三种构建graph的途径:
        1.filter graph manager构建整个graph
        2.filter graph manager构建部分graph
        3.应用程序构建整个graph
    Filter Graph Manager构建整个graph
        如果你仅仅是想回放一个已知格式的文件,如AVI、MPEG、WAV或MP3,使用RenderFile方法。
        RenderFile方法首先寻找注册在系统中能分析源文件的filter,它使用协议名(如http://),文件扩展名或文件的头几个字节来决定选择哪一个源filter。
        Filter Graph Manager使用一个迭代过程来完成余下的graph构建。在这个迭代过程中,它逐个列出filter的输出pin上支持的媒体类型,并搜索哪个已注册的filter的输入Pin接受该媒体类型。它使用一系列的规则来缩小filter的范围并排定优先顺序:
        *filter类别 (category)标识的filter的一般功能
        *媒体类型描述filter能在接受或能输出哪种数据类型
        *merit 值决定filter被尝试的次序。如果两个filter具有相同的filter类别并且同时支持相同的输入类型,Filter Graph Manager选择merit值大的那一个。一些filter故意给出一个小merit值是因为它是为特殊用途设计的,仅能由应用程序来将其添加到 graph。
        Filter Graph Manager使用Filter Mapper对象来搜索已注册的filter。
        每个filter被添加时,filter graph manager试着将其与前一个filter的输出pin连接。它们协商决定他们是否能连接,如果能,哪一种媒体类型被用来连接。如果新filter不能连接,filter graph manager丢弃它并尝试别一个,这个过程一直继续到每个流都被render为止。
    Filter Graph Manager构建部分graph
        如果不仅仅是播放一个文件,那么你的应用程序就必须做一些graph的构建工作。比如,一个视频采集应用程序必须先选择一个source filter并将其添加到graph中去。如果你需要将数据写入到一个AVI文件中,你必须添加一个AVI Mux和File Write filter。不过,也经常有可能让filter graph manager来完成整个graph,比如,你可以通过Render方法来render一个pin进行预览。
    应用程序构建整个graph
        在某些场合,你的应用程序需要添加和连接每个filter来构建graph。在这种情况下,你很可能明确地知道哪些filter需要加到graph中去。使用这种方式,应用程序通过调用 AddFilter方法添加每个filter,然后枚举filter上的pin,调用Connect或ConnectDirect来连接它们。

    3.3.3. 智能连接
        智能连接是filter graph manager用以构建filter graph的机制。它包含了一系列相关的用以选择filter和将它们添加到graph中去的算法。作为应用程序开发者,你并不需要很具体地了解智能连接的细节。如果你在构建某个filter graph时遇到问题并希望能解决它,或者你正在编写你自己的filter并希望它能自动地被graph构建,请阅读这一节。
        智能连接涉及以下IGraphBuilder方法:
        *IGraphBuilder::Render
        *IGraphBuilder::AddSourceFilter
        *IGraphBuilder::RenderFile
        *IGraphBuilder::Connect
        Render 方法构建一部分graph,它从一个尚未连接的输出pin开始顺着数据流的方向往下,添加必要的filter,起始的那个filter必须已被添加到了 graph中。Render方法每一步都搜索一个能够连接到前一个filter的filter,如果新连接上的filter有多个输出pin,数据流能自动分流,搜索直到每个流都被renderer为止。如果Render方法搜索到的filter无法使用,它会返回去尝试另一个filter。
        要连接每一个输出pin,Render方法做以下工作:
        1.如果pin支持IStreamBuilder接口,Filter Graph Manager让pin的IStreamBuilder::Render方法来完成整过程。通过暴露这个接口,pin承担了构建graph剩余部分的全部工作。但是,只有很少数的filter支持此接口。
        2.Filter Graph Manager尝试使用任何在缓存中的filter。在智能连接的整个过程中,filter graph manager可以在早期将filter缓存起来。
        3.如果filter graph包含了任何有未连接的输入pin的filter,filter graph manager会将其当作下一个filter来尝试连接。你可以通过在调用Render之前添加特定的filter来强制让Render方法来尝试这个 filter。
        4.最后,filter graph manager使用IFilterMapper2::EnumMatchingFilters方法在所有注册的filter中寻找,依据已注册的媒体类型列表来逐个试着匹配输出pin的各个媒体类型(按优先级高低排列)。
        每个已注册的filter都有一个merit值,这是一个用来表示 filter优先级的数字,最大优先级越高,EnumMatchingFilters方法返回的filter集依据merit值来排列,直至最小的 merit值MERIT_DO_NOT_USE+1,它忽略merit为MERIT_DO_NOT_USR或更小的filter。filter也通过 GUID来归类,类别本身也有merit值,EnumMatchingFilters方法忽略任何merit值为MERIT_DO_NOT_USE或更小的类别,即使在那个类别中的filter有较高的merit值。
        总结一下,Render方法以下列步骤尝试filter
        1. 使用IStreamBuilder
        2.尝试被缓存的filter
        3.尝试已添加在graph中的filter
        4. 在已注册的filter中寻找
        AddSourceFilter方法添加一个能render特定文件的source filter。首先,它依据协议名(如Http://)、文件扩展名、或文件头在已注册的filter中寻找匹配的那个。如果此方法定位到了一个合适的 source filter,它便立刻创建一个这个filter的实例,并将其添加到graph中,然后调用filter的 IFileSourceFilter::Load方法。
        RenderFile方法依据一个文件名来构建一个默认的回放graph,在其内部,RenderFile方法调用AddSourceFilter来定位source filter,并且用Render来构建Graph的余下部分。
        Connect 方法将输出pin连接到输入pin上去,这个方法自动添加必要的中间filter到graph中去,使用在Render方法中描述的那一系列算法:
        1. 使用IStreamBuilder
        2.尝试被缓存的filter
        3.尝试已添加在graph中的filter
        4. 在已注册的filter中寻找


    --  作者:admin
    --  发布时间:2005-11-26 2:51:00
    -- 

    3.4. Filter Graph中的数据流
        这一节主要描述媒体数据是如何在filter graph中流动的。如果你只是为了编写DirectShow应用程序,你不需要知道这些细节,当然,知道这些细节对于编写directshow应用程序仍然是有帮助的。但是如果你要编写directshow filter,那么你就必须掌握这部分知识了。
    3.4.1. DirectShow数据流概述
        在这一部分先粗略地描述一下DirectShow中数据流是如何工作的。
        数据首先是被保存在缓冲区里的,在缓冲区里,它们仅仅是一个字节数组。每一个缓冲区被一个称作媒体样本(media sample) 的COM对象所包容,media sample提供IMediaSample接口。media sample由另一个称作分配器(allocator) 的COM对象创建,allocator提供IMemAllocator接口。每一个pin连接都指定有一个allocator,当然,两个或多个pin连接也可以共享几个allocator。

        每一个allocator都创建一个media sample池,并为每个sample分配缓冲区。一旦一个filter需要一个缓冲区来填充数据,它就调用IMemAllocator::GetBuffer方法来请求一个sample。只要allocator有一个sample还没有被任何filter使用,GetBuffer方法就立即返回一个sample 的指针。如果allocator所有的sample已经被用完,这个方法就阻塞在那里,直到有一个sample变成可用的了。GetBuffer返回一个 sample后,filter就将数据写入到sample的缓冲区中去,并在sample上设置适当的标记(如时间戳),然后将它递交到下一个 filter去。
        当一个renderer filter接收到了一个sample时,renderer filter检查时间戳,并将sample先保存起来,直到filter graph的参考时钟指示这个sample的数据可以被render了。当filter将数据render后,它就将sample释放掉,此时 sample并不立即回到allocator的sample池中去,除非这个sample上的参考计数已经变为0,表示所有的filter都已释放这个 sample。

        上游的filter可能在renderer之前运行,这就意味着,上游的filter填充缓冲的速度可能快于 renderer销毁它们。但是尽管如此,samples也并无必要更早地被render,因为renderer将一直保存它们直到适当的时机去 render,并且,上游filter也不会意外地将这些samples的缓冲覆盖掉,因为GetSample方法只会返回那些没有被使用的 sample。上游filter可以提前使用的sample的数量取决于allocator分配池中的sample的数量。
        前面的图表只显示了一个allocator,但是通常的情况下,每个流中都会有多个allocator。因此,当renderer释放了一个sample时,它会产生一个级联效应。如下图所示,一个decoder保存了一个视频压缩帧,它正在等待renderer释放一个sample,而parser filter也正在decoder去释放一个sample。

        当renderer释放了一个sample后,decoder完成尚未完成的GetBuffer调用。然后decoder便可以对压缩的视频帧进行解码并释放它保存的sample,从而使parser完成它的GetBuffer调用。
    3.4.2. 传输协议(Transports)
        为了使媒体数据能在filter graph中流动,Directshow filter必须能支持多个协议中的一个,这些协议被称作传输协议(transports) 。当两个filter连接后,它们必须支持同一个传输协议,否则,它们将不能交换数据。通常,一个传输协议要求某个pin支持一个特定的接口,当两个 filter连接时,另一个pin来调用这个pin的这个接口。
        大多数的directshow filter在主存中保存媒体数据,并且通过pin连接向另一个filter递交数据,这种类型的传输协议被称作本地内存传输协议(local memory transport) 。尽管这类传输协议在directshow中应用最普遍,但并非所有的filter都使用它。例如,某些filter通过硬件途径来传递数据,使用pin仅仅是为了传递控制信息,如IOverlay接口。
        DirectShow为本地内存传输协议定义了两种机制,推(push)模式拉 (pull)模式 。在推模式中,source filter产生数据,并将其递交给下游的filter,下游的filter被动地接收数据并处理它们,再将数据传递给它的下游filter。在拉模式中,source filter与一个parser filter连接,parser filter向source filter请求数据,source filter回应请求并传递数据。推模式使用IMemInputPin接口,而拉模式使用IAsyncReader接口。
        推模式比拉模式应用更广泛。
    3.4.3. 媒体样本(sample)和分配器(allocator)
        当一个pin向另一个pin传递媒体数据时,它并不是直接传递一个内存缓冲区的指针,而是传递一个COM对象的指针,这个COM对象管理着内存缓冲,被称为媒体样本(media sample) ,暴露IMediaSample接口。接收方pin通过调用IMediaSample接口的方法来访问内存缓冲,如IMediaSample::GetPointer,IMediaSample::GetSize和IMediaSample::GetActualDataLength。
        sample 总是从输出pin到输入pin向下传输。在推模式中,输出pin通过在输入pin上调用IMemInputPin::Receive方法来传递一个 sample。输入pin或者在Receive方法内部同步地处理数据,或者另开一个工作线程进行异步处理。如果输入pin需要等待资源,允许在 Receive中阻塞。
        另一个用来管理媒体样本的COM对象,被称作分配器(allocator) ,它暴露IMemAllocator接口。一旦一个filter需要一个空闲的媒体样本,它调用IMemAllocator::GetBuffer方法来获得sample的指针。每一个pin连接都共享一个allocator,当两个pin连接时,它们协商决定哪个filter来提供 allocator。pin可以设置allocator的属性,比如缓冲的数量和每个缓冲的大小。
        下图显示了allocator、 media sample和filter的关系:

    媒体样本参考计数(Media Sample Reference Counts)
        一个allocator创建的是一个拥有有限个sample的sample池。在某一时刻,有些sample正在被使用,有些则可被GetBuffer方法使用。allocator使用参考计数来跟踪sample,GetBuffer方法返回的sample参考计数为1,如果参考计数变为0,sample就可以返回到allocator的sample池中去了,这样它就可以再次被GetBuffer方法使用。在参考计数大于0期间,sample是不能被 GetBuffer使用的。如果每个从属于allocator的sample都在被使用,则GetBuffer方法会被阻塞直至有sample可以被使用。
        举个例子,假设一个输入pin接收到一个sample。如果它同步地在Receive方法内部处理它,sample的参考计数不增加,当Receive返回时,输出pin释放这个sample,参考计数归0,sample就返回到sample池中去了。另一种情况,如果输入pin异步地处理sample,它就在Receive方法返回前将sample的参考计数加1,此时参考计数变为2。当输出pin释放这个sample时,参考计数变为1,sample不能返回到sample池中去,直到异步处理的工作线程完成工作,调用Release释放这个sample,参考计数变为0时,它才可以返回到sample池中去。
        当一个pin接收到一个sample,它可以将数据拷贝到另一个sample中去,或者修改原始的 sample并将其传递到下一个filter中去。一个sample可能在整个graph长度内被传递,每个filter都依次调用AddRef和 Release。因而,输出pin在调用Receive后一定不能重复使用同一个sample,因为下游的filter可能正在使用这个sample。输出pin只能调用GetBuffer来获得新的sample。
        这个机制减少了总的内存分配过程,因为filter可以重复使用同样的缓冲。它同样防止了数据在被处理前意外地被覆盖写入。
        当filter处理数据后数据量会变大(如解码数据),一个filter可以为输入 pin和输出pin分配不同的allocator。如果输出数据并不比输入数据量要大,filter可以用替换的方式来处理数据而不用将其拷贝到新的 sample中去,在这种情况下,两个或多个pin连接共享一个allocator。
    提交 (Commit)和反提交(Decommit)分配器
        当一个filter首次创建一个allocator 时,allocator并不为其分配内存缓冲,此时如果调用GetBuffer方法的话会失败。当流开始流动时,输出pin调用IMemAllocator::Commit来提交allocator,从而为其分配内存。此时pin可以调用GetBuffer了。
        当流停止时,pin调用IMemAllocator::Decommit来反提交allocator,在allocator被再次提交前所有后来的GetBuffer调用都将失败,同样,如果有阻塞的正在等待sample的 GetBuffer调用,也将立即返回失败信息。Decommit方法是否释放内存取决于实现方式,如CMemAllocator类直至析构时才释放内存。
    3.4.4. filter状态
        filter有三种可能的状态:停止(stopped)就绪(paused)运行(running) 。就绪状态的目的是为了让graph提前做准备以便在run命令下达时可以立即响应。Filter Graph Manager控制所有的状态转换。当一个应用程序调用IMediaControl::Run,IMediaControl::Pause或IMediaControl::Stop时,Filter Graph Manager在所有filter上调用相应的IMediaFilter方法。在停止状态和运行状态之间转换时总是要经过就绪状态,即如果应用程序在一个处于停止状态的graph上调用Run时,Filter Graph Manager在运行它之前先将其转为pause状态。
        对于大多数 filter来说,运行状态和就绪状态是等同的。看下面的这个graph:
        Source > Transform > Renderer
        假设这个source filter不是一个实时采集源,当source filter就绪时,它创建一个线程来尽可能快地产生新数据并写入到media sample中去。线程通过在transform filter的输入pin上调用IMemInputPin方法将sample“推”到下游filter。transform filter在source filter的线程中接收数据,它可能也使用一个工作线程赤将sample传递给renderer,但是在通常情况下,它在同一个线程中传递它们。如 renderer处理就绪状态下,它等待接收sample,当它接收到一个时,它或阻塞或保存那个sample,如果这是一个Video renderer,则它将sample显示为一个静态的图片,只在必要的时候刷新它。
        此时,流已经准备充分去被render,如果 graph仍然处理就绪状态下,sample会在每一个sample后堆积,直至每个filter都被阻塞在Receive或GetBuffer下。没有数据会被丢失。一旦source线程的阻塞被解除时,它只是简单地从阻塞点那里进行恢复。
        source filter和transform filter忽略从就绪状态转到运行状态——它们仅仅是尽可能快地继续处理数据。但是当renderer运行时,它就要开始render sample了。首先,它render在就绪状态下保存的那个sample,接着,每接收到一个新的sample,它计算这个sample的呈现时间,renderer保存每个sample直至到了它们的呈现时间再render它们。在等待合适的呈现时间时,它或者阻塞在Receive方法上,或者在一个工作线程中接收数据并将其放入队列中去。renderer的上一个filter不关心这些问题。
        实时源(live source),如采集设备,是通常情况中的一个例外。在实时源中,不适合提前准备数据。应用程序可能将graph置于就绪状态下,然后等很长时间才再运行它。graph不应该再render就绪期间的sample,因此,一个实时源在就绪状态时不产生新的sample。要将这种情况通知给filter graph manager,source filter的IMediaFilter::GetState方法返回VFW_S_CANT_CUE。这个返回值表示filter已切换到就绪状态下,即使renderer还没有收到任何数据。
        当一个filter停止时,它不再接收任何传递给它的数据。source filter关闭它们的流线程,别的filter关闭所有它们创建的工作线程。pin反提交(decommit)它们的allocator。
    状态转换
        filter graph manager按从下游filter到上游filter的次序来完成所有的状态转换,从renderer开始逐个向上直至source filter,这个次序是必要的,可以防止数据丢失或graph死锁。最重要状态转换是就绪状态和停止状态间的转换:
        *停止状态到就绪状态:当每一个filter被置为就绪态时,它便准备好从上一个filter接收sample。source filter是最后一个被置为就绪态的filter,它创建数据流线程并开始传递sample。因为所有下游filter都处于就绪状态,所以没有一个 filter会拒绝接收sample。当graph中所有的renderer都接收到一个sample后,filter graph manager才彻底完成状态转换工作(实时源除外)。
        *就绪状态到停止状态:当一个filter停止时,它释放了所有它保存的 sample,就将解除所有上游filter调用GetBuffer时的阻塞。如果filter正在Receive方法中等待数据,则它停止等待并从 Receive中返回,从而解除阻塞。因而,此时当filter graph manager再去将上游filter转换为停止状态时,它已经不再阻塞于GetBuffer和Receive,从而可以响应停止命令。上游filter 在得到停止命令前可能会传递下一些过时的sample,但下游filter不再接收它们,因为此时下游filter已处于停止状态了。
    3.4.5. 拉模式
        在IMemInputPin接口中,上游filter决定哪些数据要被发送,然后将数据推到下游filter 中去。但是在某些情况下,拉模式会更加合适。在拉模式中,只有当下游filter从上游filter中请求数据时,数据才被传递下去,数据流动由下游 filter发起。这种类型的连接使用IAsyncReader接口。
        典型的拉模式应用是文件回放。比如,在一个AVI回放graph中,Async File Source filter完成一般的文件读操作并将数据作为字节流传递下去,没有什么格式信息。AVI Splitter filter读取AVI头并将数据流分解成视频和音频sample。AVI Splitter比Async File Source filter更能决定它们需要哪些数据,因此需用IAsyncReader接口来代替IMemInputPin接口。
        要从输出pin请求数据,输入pin调用下面方法中的一个:
        *IAsyncReader::Request
        *IAsyncReader::SyncRead
        *IAsyncReader::SyncReadAligned
        第一个方法是异步的,支持多重读操作。其余的是同步的。
        理论上,任一个filter都能支持IAsyncReader,但是实际上,它仅仅在连接有一个parser filter的source filter上使用。分析器(parser)非常象一个推模式的source filter,当它就绪时,它创建一个数据流线程,从IAsyncReader连接中拉数据并将其推到下一游filter中去。它的输出pin使用 IMemInputPin,graph余下的部分使用标准的推模式。


    --  作者:admin
    --  发布时间:2005-11-26 2:52:00
    -- 

    3.5 DirectShow中的事件通告
        这一节主要描述在 directshow filter graph中事件是怎样发生的,以及应用程序如何接收事件通告并响应它们。
    3.5.1 概述
        一个filter通过发送一个事件通来通知filter graph manager某个事件已经发生。这些事件可以是一些预知的事件比如流结束事件,也可以是一些异常如render流时失败。一部分事件由filter graph manager自己处理,另一部分则由应用程序来处理。如果filter graph manager不处理某个事件,那么这个事件会被放入到队列中去。filter graph也可以通过队列将自己的事件发送给应用程序。
        应用程序从队列中接收事件并根据其类型来响应它们。DirectShow中的事件通告类似于windows的消息队列机制。应用程序可以让filter graph manager取消对指定的事件类型的默认操作,而是将它们放入事件队列由应用程序来处理它们。
        由于这样的机制,使我们能做到:
         *filter graph manager与应用程序的对话
         *filter可以即和应用程序也和filter graph manager对话
         *由应用程序来决定处理事件的复杂度。

    3.5.2 从队列中取事件
        Filter Graph Manager暴露3个支持事件通知的接口:
         *IMediaEventSink 包含filter发送事件的方法
         *IMediaEvent 包含应用程序取事件的方法
         *IMediaEventEx 继承扩展IMediaEvent接口
        filter 通过在filter graph manager上调用IMediaEventSink::Notify方法来发送事件通告,一个事件通知由一个表示事件类型的事件号,和两个DWORD类型用以放置附加信息的参数组成。按事件号的不同,这两个参数可以是指针、返回值、参考时间或者其它信息。完整的事件号和参数列表,参见Event Notification codes(http://msdn.microsoft.com/library/en-us/directshow/htm/eventnotificationcodes.asp )。
        要从事件队列中取事件,应用程序需要在filter graph manager上调用IMediaEvent::GetEvent事件。这个方法一直阻塞到取到事件或超时。一旦队列中有了事件,这个方法就返回事件号和两个事件参数。在调用GetEvent后,应用程序应该总是调用IMediaEvent::FreeEventParams方法来释放与事件参数相关的所有资源。比如,一个参数可能是由filter graph分配的BSTR值。
        下面的代码是一个如何从队列中取事件的框架:

      long evCode, param1, param2;
      HRESULT hr;
      while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr))
      {
          tch(evCode)
          {
              // Call application-defined functions for each
              // type of event that you want to handle.
          }
          hr = pEvent->FreeEventParams(evCode, param1, param2);
      }

      要重置filter graph manager默认的事件处理过程,调用IMediaEvent::CancelDefaultHandling方法,用事件号做参数。你可以通过调用 IMediaEvent::RestoreDefaultHandling方法来恢复某个事件的处理过程。如果filter graph对某个事件号没有默认处理过程,则调用上面两个方法不产生任何影响。

    3.5.3 当事件发生时
        要处理DirectShow事件,应用程序需要一个方法来知道事件何时正等待在队列中。Filter Graph Manager提供两种方法:
        *窗口通告:一旦有事件发生,Filter Graph Manager就发送一个用户自定义窗口消息来通知应用程序窗口
        *事件信号:如果有DirectShow 事件在队列中,filter graph manager就触发一个windows事件,如果队列为空,则reset这个事件。
        应用程序可以使用任何一种方法,但通常窗口通告方法相对比较简单。
    窗口通告:
        要设置窗口通告,调用IMediaEventEx::SetNotifyWindow方法并指定一个私有消息,私有消息可以是从WM_APP到0xBFFF的任一个。一旦filter graph manager把一个新的事件通告放入队列中,它便发送这个消息给指定的窗口。应用程序从窗口的消息循环中来响应这个消息。
        下面是如何设置通知窗口的例子:

       #define WM_GRAPHNOTIFY WM_APP + 1   // Private message.
      pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);

      消息是一个普通的windows消息,并且独立于DirectShow消息通告队列被发送。使用这种方法的好处是大部分应用程序拥有一个消息循环,因此,要知道DirectShow事件何时发生便无需做额外的工作了。
      下面是一段如何响应通告消息的框架代码:

       LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, UINT wParam, LONG lParam)
      {
          tch (msg)
          {
              case WM_GRAPHNOTIFY:
                  HandleEvent();  // Application-defined function.
                  break;
              // Handle other Windows messages here too.
          }
          return (DefWindowProc(hwnd, msg, wParam, lParam));
      }

          因为事件通告与消息循环均为异步进行的,因此在应用程序响应事件时队列中可以会有多个事件。而当事件变为非法时,它们会从队列中被清除掉。所以在你的事件处理代码中,调用GetEvent直至返回一个表示队列已空的失败代号。
        在释放 IMediaEventEx指针前,请以NULL作参数调用SetNotifyWindow方法来取消事件通告。并且在你的事件处理代码中,在调用 GetEvent前检查IMediaEventEx指针是否合法。这些步骤可以防止在释放IMediaEventEx指针后应用程序继续接收事件通告的错误。
    事件信号:
        Filter Graph Manager建立一个反映事件队列状态的手工重设事件(manual-reset event)。如果队列中包含有未处理的事件通告,Filter Graph Manager就会发信号给手工重设事件。如果队列是空的,则调用IMediaEvent::GetEvent方法会重设(reset)事件。应用程序可以通过这个事件来确定队列的状态。
        注意:此处的术语可能被混淆。手工重设事件是由windows的 CreateEvent函数创建的一种事件类型,它与由DirectShow定义的事件无关。
        调用 IMediaEvent::GetEventHandle方法得到手工重设事件的句柄,调用一个函数如WaitForMultipleObjects来等待发送给手工重设事件的信号。一旦收到信号,就可以调用IMediaEvent::GetEvent来接收DirectShow事件了。
        下面的代码举例说明了这种方法。在取得事件句柄后,在100毫秒时间间隔内等待发送给手工重设事件的信号,如果有信号发来,它调用GetEvent然后在 windows控制台上打印出事件号和事件参数,循环在EC_COMPLETE事件发生后结束,这标志着回放结束。

    HANDLE  hEvent;
      long    evCode, param1, param2;
      BOOLEAN bDone = FALSE;
      HRESULT hr = S_OK;
      hr = pEvent->GetEventHandle((OAEVENT*)&hEvent);
      if (FAILED(hr)
      {
          /* Insert failure-handling code here. */
      }
      while(!bDone)
      {
          if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent, 100))
          {
              while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr))
              {
                  printf("Event code: %#04x//n Params: %d, %d//n", evCode, param1, param2);
                  pEvent->FreeEventParams(evCode, param1, param2);
                  bDone = (EC_COMPLETE == evCode);
              }
          }
      }

        因为Filter Graph会在适当的时候自动重设事件,因此你的应用程序应当不去作重设工作。同时,当你释放filter graph时,filter graph会关闭事件句柄,因此在这之后你就不能再使用事件句柄了。


    --  作者:admin
    --  发布时间:2005-11-26 2:53:00
    --  
    3.6. DirectShow中的时间和时钟
        这一节主要概述DirectShow体系中时间和时钟。
    3.6.1. 参考时钟
        Filter Graph Manager的一个功能,能够以同一个时钟来同步所有在graph中的filter,称作参考时钟(reference clock)。
        任何暴露了IReferenceClock接口的对象都能够作为一个参考时钟来使用。参考时钟可以由一个 DirectShow filter来提供,例如可以直接使用硬件时钟的audio renderer。另外,Filter Graph Manager也能使用系统时间来作参考时钟。
        名义上,一个参考时钟以千万分之一秒的精度来度量时间,但是实际上的精度不会这么高。要取得参考时钟的当前时间,调用IReferenceClock::GetTime方法。由于时钟的基准时间,即时钟开始时的时间计数,是依赖于具体的实现的,因此GetTime的返回值不反映绝对时间,只反映相对于graph开始时的相对时间。
        虽然参考时钟的精度是变化的,但是 GetTime的返回值却保证是单调递增的,换句话说,也就是参考时钟的时间是不会回退的。如果参考时钟的时间是由硬件源产生的,而硬件时钟回退了(比如,有一个调节器调节了时钟),GetTime依然返回最晚的那个时间只到硬件时钟追上它。要知道更多的内容可以参考 CBaseReferenceClock类(http://msdn.microsoft.com/library/en-us/directshow/htm/cbasereferenceclockclass.asp )。
    默认参考时钟
        当Graph运行时,Filter Graph Manager会自动选择参考时钟,选择参考时钟的规则如下:
        *如果应用程序指定了一个时钟,则使用这个时钟;
        *如果 Graph包含了一个支持IReferenceClock的活动源filter(即推模式源filter),则使用这个filter;
        *如果Graph未包含任何支持IReferenceClock的推模式源filter,使用任何一个支持IReferenceClock接口的 filter,选择的次序是从Renderer filter开始依次向上。已连接的filter优先于未连接的filter被选。(如果这个graph会render一个音频流,则这个规则通常就会选择audio renderer filter来作为参考时钟)
        *如果没有filter支持合适的时钟,则使用系统参考时钟。
    设置参考时钟
        应用程序可以在Filter Graph Manager上调用IMediaFilter::SetSyncSource方法来选择时钟,只有在由于你有一个特殊原因想要选择自己的时钟时才需要这么做。
        想要让Filter Graph Manager不使用任何参考时钟,可以调用SetSyncSource,参数为NULL。比如,你需要尽可能快地来处理sample时,就可以这么做。要恢复黑认的参考时钟,在Filter Graph Manager上调用IFilterGraph::SetDefaultSyncSource方法。
        当参考时钟发生变化时,Filter Graph Manager会通知每一个filter调用它的IMediaFilter::SetSyncSource方法,应用程序无需调用filter的这个方法。
    3.6.2. 时钟时间
        DirectShow定义了两种相关时间:参考时间(reference time)和流时间 (stream time)
        *参考时间是一个绝对时间,由参考时钟返回
        *流时间是一个相对于graph最后开始时的相对时间
         ·当graph处于运行态时,流时间等于参考时间减去起始时间
         ·当graph处于暂停态时,流时间停留在暂停的那一刻
         ·在重新定位后,流时间被重设为0
         ·当graph处于停止态时,流时间无意义
        如果一个媒体样本有一个时间戳t,表示这个在流时间为t时被render,正因为这个原因,因此流时间也被叫做呈现时间(presentation time)。
        当应用程序调用IMediaControl::Run运行graph时,Filter Graph Manager调用每个filter的IMediaFilter::Run。为了补偿消耗在运行每个filter的时间总和,Filter Graph Manager会略微晚一点来定义起始时间。
    3.6.3. 时间戳
        时间戳定义了媒体样本的起始和结束时间。时间戳有时被称作呈现时间(presentation time)。在阅读余下的文章时,一个必须记住的要点是并非所有的媒体格式都以相同的方式来使用时间戳。举个例子,并不是所有MPEG样本都被打上了时间戳,在MPEG Filter Graph中,时间戳在被解码前并非应用在每个帧上。
        当一个renderer filter接收到一个样本时,它以时间戳为基准来确定render时间。如果样本来晚了,或者这个样本没有时间戳,那个filter就立刻render 它,否则,filter就等在那直到合适的时机。(通过IReferenceClock::AdviseTime方法来等待样本的render时间)
        源 filter和语法解析filte使用下列原则,在它们处理的样本上设置合适的时间戳:
        *文件回放:第一个样本被打上起始时间戳,为0,后面的时间戳由样本长度和回放速率来决定,这些都由文件格式来决定。分析文件的filter负责计算出合适的时间戳。例子见(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directshow/htm/avisplitterfilter.asp
        *音视频采集:每个样本都被打上一个起始时间戳,这个时间戳与当它被捕获时的Stream time相同。应注意以下几点:
         ·从预览pin(Preview Pin)出来的样本没有时间戳。因为图像处理的延时,一个打上采集时间的视频帧总是会迟一点到达视频renderer。这会导致在进行质量控制时,renderer会丢弃部分视频帧。关于质量控制,参见(http://msdn.microsoft.com/library/en-us/directshow/htm/qualitycontrolmanagement.asp
         ·音频采集:音频采集filter使用它自己的缓冲集,而并非使用音频驱动程序的。音频驱动以固定的时间间隔来填充采集filter的缓冲。这个时间间隔由驱动决定,通常不超过10毫秒。在音频样本上的时间戳反映的是驱动填充采集filter时的时间,因此会有偏差,尤其是当应用程序使用一个很小的缓冲区时。不过,媒体时间可以精确地反映缓冲区中音频样本的数量。
        *混合filter(Mux filter):依赖于输出格式,一个mux filter可能需要由它来产生时间戳,也可能不需要。举个例子,AVI文件格式使用固定的帧率而没有时间戳,因此AVI Mux filter假设那些样本在近似正确的时间内到达。如果样本没有在合适的时间间隔内到达,AVI Mux filter会插入一个长度为0的空样本,来表示一个丢失的帧。在文件回放时,新的时间戳在运行时如前面所述地那样产生。
        要在一个样本上设置一个时间戳,调用IMediaSample::SetTime方法。
        此外,filter还可以为样本指定一个媒体时间(media time)。在视频流中,media time表示视频帧的数量。在音频流中,media time表示包中的样本数量,比如,如果每个包包含以44.1KHz的采样率采集的一秒钟的音频,那么第一个包具有一个为0的媒体起始时间以及为 44100的媒体终止时间。在一个可以定位的流中,媒体时间总是相对于流的起始时间,比如,假设你在一个15帧/秒的视频流上定位到2秒这个位置,那么定位后的每一个媒体样本的时间媒为0,但是它的媒体时间为30.
        Renderer和Mux filter能使用媒体时间通过检查是否有缺口来确定帧或样本是否被丢弃了。但是,filter不是一定要设定媒体时间。要设置媒体时间,调用 IMediaSample::SetMediaTime方法。
    3.6.4 实时源(Live Source)
        实时源,也被叫做推模式源(push source),实时地接收数据,比如视频采集和网络广播。通常情况下,一个实时源不能控制数据到达的速率。
        一个filter被认为是实时源需要具有以下几点:
        * 调用IAMFilterMiscFlags::GetMiscFlags方法时返回AM_FILTER_MISC_FLAGS_IS_SOURCE标记,并且至少有一个输出pin暴露IAMPushSource接口。
        * filter暴露IKsPropertySet接口,并具有一个capture pin(PIN_CATEGORY_CAPTURE)。
    延时(latency)
        一个filter的延时是这个filter处理一个样本所需的时间总和。在实时源中,延时取决于保存样本的缓冲区大小。举个例子,假设graph有一个具有33ms延时的视频源和一个具有500ms延时的音频源,那么每个到达视频renderer的视频帧要比与之匹配的音频样本到达音频renderer早470ms,除非 graph对这个差别进行补偿,否则音视频将会不同步。
        实时源可以通过IAMPushSource接口来进行同步。Filter Graph Manager并做同步工作除非应用程序通过调用IAMGraphStreams::SyncUsingStreamOffset方法来激活它。如果同步被激活,Filter Graph Manager通过IAMPushSource来查询每一个source filter,如果filter支持IAMPushSource,那么Filter Graph Manager调用IAMLatency::GetLatency来得到filter预期的延时(IAMPushSource继承自 IAMLatency)。通过组合的延时值,filter graph manager决定graph中最大的预期延时,然后调用IAMPushSource::SetStreamOffset来给每一个source filter一个流偏移,以后filter会在产生时间戳时加上这个偏移。
        这个方法主要是为了实现实时预览,但是,注意实时采集设备(比如摄像头)的preview pin上是没有时间戳的,因此,要在一个实时采集设备上使用这种方法,你必须在capture pin上进行视频预览。
        通常,IAMPushSource接口被VFW Capture filter和音频采集filter(Audio capture filter)支持。
    速率匹配(Rate Matching)
        如果 renderer filter和source filter使用不同的参考时钟,那么就会有问题,renderer可能比source要快,这就导致了数据的缺口,或则renderer比source 慢,就会导致数据拥堵而样本丢弃。通常一个实时源无法控制速率,因此要求renderer来与source进行速率匹配。
        通常,只有 audio renderer实现速率匹配,因为声音回放的频率比视频更重要。要实现速率匹配,audio renderer必须排除以下几点:
        *如果graph没有使用一个参考时钟,那么audio renderer不会去进行速率匹配(如果graph没有参考时钟,那么样本总是在到达时就被立刻render)。
        *另外,如果 graph中有一个参考时钟,audio renderer检测是否有一个实时源在上游,如果没有,audio renderer不进行速率匹配。
        *如果有一个实时源在上游,并且这个实时源在它的输出Pin上暴露IAMPushSource接口,audio renderer调用IAMPushSource::GetPushSourceFlags,并寻找以下标记:
         ·AM_PUSHSOURCECAPS_INTERNAL_RM,这个标记表示这个实时源拥有自己的速率匹配机制,因此audio renderer不进行速率匹配。
         ·AM_PUSHSOURCECAPS_NOT_LIVE,这个标记表示source filter并不是一个真正的实时源,即使它暴露了IAMPushSource接口,因此,audio renderer不进行速率匹配。
         ·AM_PUSHSOURCECAPS_PRIVATE_CLOCK,这个标记表示source filter使用一个私有的时钟来产生时间戳。在这种情况下,audio renderer速率匹配与时间戳会有冲突。(如果样本没有时间戳,那么renderer忽略这个标记。
        *如果 GetPushSourceFlags返回没有标记(0),audio renderer的行为依赖于graph时钟和样本是否拥有时间戳:
         ·如果audio renderer不是graph参考时钟,并且样本拥有时间戳,那么audio renderer速率匹配与时间戳会有冲突
         ·如果样本没有时间戳,audio renderer尝试与输入的音频数据的速率进行匹配。
         ·如果audio renderer是graph参考时钟,它与输入的数据速率进行匹配。
        最后一种情况的原因如下:如果audio renderer是参考时钟,并且source filter使用同样的时钟来产生时间戳,那么audio renderer不会与这个时间戳进行速率匹配,因为如果它这样做了,导致的结果是,它等于在尝试与自己进行速率匹配,这将导致时钟偏差。因此,在这种情况下,renderer与输入的音频数据速率进行匹配。


    --  作者:admin
    --  发布时间:2005-11-26 2:53:00
    -- 

    3.7. Graph动态重建(Dynamic Graph Building)
        如果你需要修改一个已经存在的filter graph,你可以停止,修改后再重新启动它。这通常是一种最佳的解决方法。但是,在某此情况下,你可能需要在一个graph处于运行状态时来修改它,比如:
        *应用程序在进行视频回放时需要插入一个(视频滤镜filter)Video effect filter;
        *source filter在播放的过程中改变了媒体格式,此时可能需要接入新的解码filter;
        *应用程序在graph中加入一个新的视频流。
        上面的这些都是graph动态重建的例子。所有在graph继续处于运行状态而做的graph修改都被叫做graph动态重建。动态重建可以由应用程序发起,也可以由一个在graph中的filter发起。动态重建有三种可能:
        *媒体格式动态变化:一个filter可以在运行的中途改变媒体格式,而不需要重新被替换为另一个;
        *动态重连:在graph中添加或删除filter
        *Filter Chain操作:添加,删除,控制filter chain,(Filter Chain是相互连接着的一条Filter链路,并且链路中的每个Filter至多有一个Input pin,至多有一个Output pin)
    3.7.1. 动态重连
        在绝大多数的directshow filter中,当graph处于运行状态时pin是不能被重新连接的,应用程序必须在重连前停止graph。但是,某些filter却支持动态重连,这既可以由应用程序来执行,也可以由graph中的一个filter来执行。
        如下图:
       

        假设我们要将filter 2从graph中移除掉,替换成另一个filter,而此时graph还处于运行状态,那么必须具备以下几个条件:
        *filter 3的输入pin(pin D)必须支持IPinConnection接口,这个接口可以重新连接pin而不需要停止它。
        *filter 1的输出pin(pin A)必须能够在重连时阻塞媒体数据,数据不再在pin A和pin D之间传递。也就是说,输出Pin必须支持IPinFlowControl接口。但是,如果filter 1是发起重连的那个filter,那么它有可能已经在其内部实现了阻塞;
        动态重连包括下列步骤:
        1. 从Pin A那里阻塞数据流
        2. 重新连接Pin A和Pin D,或者在中间加入新的filter
        3. 取消Pin A上的阻塞

    步骤1. 阻塞数据流     通过调用Pin A上的IPinFlowControl::Block方法来阻塞数据流。这个方法既可以被同步调用,也可以被异步调用。要异步调用这个方法,需要创建一个win32事件对象,并将事件句柄传给Block,方法会立即返回,然后使用 WaitForSingleObject或其它函数来等待事件的触发。当阻塞工作完成时,pin会触发这个事件。如:

    // Create an event
    HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    if (hEvent != NULL)
    {
        // Block the data flow.
        hr = pFlowControl->Block(AM_PIN_FLOW_CONTROL_BLOCK, hEvent);
        if (SUCCEEDED(hr))
        {
            // Wait for the pin to finish.
            DWORD dwRes = WaitForSingleObject(hEvent, dwMilliseconds);
        }
    }

        如果是同步调用Block,那么只需将传入的hEvent参数设为NULL,此时这个方法会一直阻塞到阻塞工作完成为止。如果pin还没有准备好deliver一个新的sample,那么就会一直阻塞。而如果filter处于就绪状态,这可能会花费任意长的时间,因此,不要在应用程序的主线程中使用同步调用,以免发生死锁,开一个工作线程来使用同步调用,或者干脆就使用异步调用。

    步骤2. 重连pin
        要重新连接pin,查询graph的IGraphConfig接口并调用IGraphConfig::Reconnect或IGraphConfig::Reconfigure。 Reconnect方法使用比较简单:
        *停止中间filter(比如filter 2),并移除它
        *如果需要的话,加入新的中间filter
        *连接所有的pin
        *pause或run所有新的filter,使它的状态与graph相同
        Reconnect 方法有参数可以用来指定pin连接的媒体类型和中间filter。如:

    pGraph->AddFilter(pNewFilter, L"New Filter for the Graph");
    pConfig->Reconnect(
        pPinA,      // Reconnect this output pin...
        pPinD,      // ... to this input pin.
        pMediaType, // Use this media type.
        pNewFilter, // Connect them through this filter.
        NULL,
        0);

        如果Reconnect还不够用来应付我们的要求,那么你可以使用Reconfigure方法,它调用一个由应用程序定义的回调函数来重连这些 pin。要调用这个方法,需要在你的应用程序中实现IGraphConfigCallback接口。
        在调用Reconfigure之前,如前面所述地那样阻塞输出pin的数据流。然后如下所示,将处于待处理状态的数据push下去:
        1. 在重连链路中处于下游的最远的那个输入pin(例子中为Pin D)上调用IPinConnection::NotifyEndOfStream方法,方法的参数是一个Win32事件句柄;
        2. 在与要阻塞数据的那个输出pin直接相连的那个输入pin上调用IPin::EndOfStream方法。(在例子中,要阻塞的那个输出pin是pin A,那么直接与之相连的那个输入pin为Pin B);
        3. 等待事件触发。输入pin(pin D)在它接收到end-of-stream事件通告时触发事件。这表示再没有数据需要传输,此时就可以安全地进行重连了。
        注意:IGraphConfig::Reconnect方法会自动处理上述步骤,你仅在调用Reconfigure方法时才需要自己来处理。
        当数据完成push后,调用Reconfigure,传入IGraphConfigCallback回调函数的指针。Filter Graph Manager会调用IGraphConfigCallback::Reconfigure方法。

    步骤3. 取消数据流的阻塞
        当你完成重连后,通过调用IPinFlowControl::Block,第一个参数为0来取消阻塞。

        注意:如果动态重连是由一个filter来执行的,那么你需要知道一点线程方面的问题。如果filter graph manager尝试去停止filter,它可能会死锁,因为graph等待filter停止,而与此同时,filter有可能在等待数据在graph中完成push。要防止这个可能存在的死锁问题,如前所述可以用事件机制来处理。

    3.7.2. filter链(filter chains)
        一个 filter chain是一系列具备下述条件的相互连接的filter:
        *每一个在链中的filter最多只有一个已连接的输入pin 和一个已连接的输出pin;
        *Filter链路中的数据流不依赖于链路外的其他Filter
        举个例子,在下图中,filter A-B,C-D和F-G-H是一个filter chains。每个F-G-H中的子链(F-G和G-H)也是一个filter chain。一个filter chain同样可以是由单个filter组成的,因此A、B、C、D、F、G和H同样也是filter chain。filter E由于有两个输入连接,所以任何含有E的一系列filter都不是filter chain。

        IFilterChain接口提供下述方法来控制filter chain:

    IFilterChain::StartChain  开启一个链
    IFilterChain::StopChain   停止一个链
    IFilterChain::PauseChain  暂停一个链
    IFilterChain::RemoveChain  从graph中移除一个链

        没有特殊的方法来添加一个链,要添加链,通过调用IFilterGraph::AddFilter方法来插入新的filter,然后调用IGraphBuilder::Connect,IGraphBuilder::Render或类似的方法来连接它们。
        当graph运行时,一个filter chain可以在运行和停止状态间切换。当graph处理就绪状态时,它可以在就绪和停止状态间切换。这是两种仅有的filter chain状态切换可能。
    Filter链指南
        当你使用IFilterChain方法时,确认在graph中的filter是否能支持filter链操作是十分必要的,否则,可能会发生死锁或graph 错误。filter连接到链上必须发生在链状态改变后。
        使用IFilterChain的最佳情况是与一系统为链而设计的filter一起使用。使用下面的指南来确保你的filter是链操作安全的。参考下图:

          在 filter链状态变化前,所在在filter链分界线上调用的数据处理都必须已完成。这个规则应用于IMemInputPin::Receive、IPin::NewSeqment和IPin::EndOfStream方法。链中的filter必须从由链外filter实现的这些方法调用中返回;而链外的filter也必须从这些由链内filter实现的这些方法调用中返回。
        举个例子,在上图中,filter B必须完成在filter A上的所有数据处理调用,而filter E也必须完成从filter D上的调用。如果pin暴露了IPinFlowControl和IPinConnection接口,那么如在动态重连那一节中所讲的,你可以通过调用IPinFlowControl::Block和IGraphConfig::PushThroughData方法来推数据。filter也可能通过自己的方法来推数据。
    上游filter必须与链的状态一起发生变化。比如,在上图中,假如链已停止,但filter A调用IMemInputPin::Receive方法,那么调用将失败,作为回应,filter A停止流。当应用程序重新开启链时,不会产生什么影响,因为filter A不再向使数据流动了。
    下游filter必须同样与链的状态一起发生变化,否则,下游filter在等待取得sample时会发生死锁,因为sample不会再到来了。比如,多路复用(MUX)filter总是在它所有的input pin上需要数据,如果挂起其中的一个input pin,在其它input pin上的流处理也会被阻塞。这会导致graph死锁
    每个与链内部filter相连的外部 filter的pin必须拥有自己的分配器(allocator),它不能被其它pin连接共享。当链的状态发生变化或从graph移除掉时,分配器便不可用了,此时如果还有其它的连接使用这个分配器的话,它们将不能再处理sample了。
    除非与链相连的filter支持动态断开,否则不要移除链。典型的,已连接的filter会支持IPinConnection或IPinFlowControl接口,或者用它自己定义的接口代替。


    --  作者:admin
    --  发布时间:2005-11-26 2:54:00
    -- 

    3.8. 插件发布者(Plug-in Distributors)

    Plug-in Distributors(PIDs)是扩展filter graph manager的一种方法。一个PID是filter graph manager在运行时聚合的一个COM对象。应用程序通过filter graph manager来进入PID。
        当filter graph manager被要求查询一个它不支持的接口时,它会搜索注册表项:
    HKEY_CLASSES_ROOT//Interface//IID//Distributor
       IID是接口的GUID,如果注册项存在,那么键值便是支持该接口的PID类标识(CLSID)。filter graph manager聚合了PID并返回接口指针,应用程序调用这个指针时实际上就是在调用PID,但是这对于应用程序来说是透明的,对于应用程序来说,它就象是在filter graph manager上调用这个接口一样。
       PID为应用程序提供了一种简单的控制filter的方法,如通过调用IFilterGraph::EnumFilters方法,PID可以枚举graph中的所有 filter并调用这些filter上的方法。
       当filter graph manager聚合了一个PID时,它查询PID的IDistributorNotify接口,如果PID支持这个接口,filter graph manager用它来通知PID有关graph的状态变化:
    * 当filter graph在run、pause和stop状态之间切换时,它调用IDistributorNotify::Run, IDistributorNotify::Pause或IDistributorNotify::Stop。
    * 如果调置了参考时钟,filter graph manager调用IDistributorNotify::SetSyncSource。
    * 当有filter添加或移除,或pin连接有变化时,filter graph manager调用IDistributorNotify::NotifyGraphChange。
       当自己定制PID时,自己所创建的COM对象必须支持聚合,并且它所支持的接口是filter graph manager本身所没有的。IDistributorNotify接口是可选的。
       如果PID从filter graph manager上获得一个接口,那它必须立即release这个接口,否则会在COM对象上出现循环引用的参考计数,使得filter graph manager无法被销毁。在filter graph manager上保持一个参考计数是多余的,因为PID的生命期是由filter graph manager控制的。
       因为PID是明确指定是被filter graph manager聚合使用的,因此你应该在PID的构造函数中强行检查IUnknown指针是否为NULL,如果为NULL,则返回错误码 VFW_E_NEED_OWNER。同时,为了防止其它对象聚合PID,你可以在IUnknown上查询IGraphBuilder接口,如果不行则返回错误。


    --  作者:admin
    --  发布时间:2005-11-26 2:55:00
    --  奇妙”的Merit(玩死Media Player)

    运行GraphEdit,插入Filter,我们可以看到:每个Filter的信息一般包括Displayname、Filename、Merit、各个Pin以及Pin支持的Mediatype,还有Version等。我们今天就来看一看这个Merit(其他的一些Filter信息大家从它的名字上就可以猜到它的意义)。
    要说Merit,肯定要先说Filter Graph Manager使用的智能连接(Intelligent Connect)机制。我们在Filter Graph中Render一个Pin,或者Render一个File,然后看到一条自动的“解码”Filter链路就完成了——这就是智能连接机制。执行这个机制的调用方法为:IGraphBuilder::RenderFile, IGraphBuilder::Render, 和 IGraphBuilder::Connect。下面分别对这三个调用方法进行阐述。
    RenderFile :给出一个文件名,首先要找到正确的Source Filter。Filter Graph Manager通过查找注册表来决定使用什么Source Filter。在注册表中,一般会有文件扩展名或者特征字节与使用的Source Filter的对应信息。找到Source Filter之后,就从该Source Filter的各个Output pin开始,进行剩下的职能连接过程。这是一个“递归”过程,直到所有的分支都连到一个Renderer Filter上。步骤大致为:
    1. 如果Output pin支持IStreamBuilder接口,则把剩下的工作交给IStreamBuilder::Render。
    2. 使用在内存中缓冲的Filter进行是试连接。
    3. 使用在当前Filter Graph中还没有完全连接的Filter进行试连接。(如果你想智能连接使用特定的你想使用的Filter,一种方法是,在开始智能连接之前先把该 Filter加入到Filter Graph中。)
    4. 使用IFilterMapper2::EnumMatchingFilters搜索注册表。Filter Graph Manager使用Merit值大于MERIT_DO_NOT_USE的所有Filter(Filter所在的目录Merit值也应该大于 MERIT_DO_NOT_USE)进行试连接。在匹配Mediatype的前提下,Merit值越高,该Filter被使用的概率越高。
    Render :这个方法从当前Filter Graph的某个Filter的指定Output pin开始,进行从这个Pin往下的一条支路的智能连接。智能连接的算法与上述RenderFile的类似。
    Connect :这个方法调用,以欲连接的一对Input pin和Output pin作为参数。首先进行这两个Pin之间的直接连接。如果不能成功连接,则要插入“中介”Filter。这个“中介”Filter的插入过程就是一个智能连接过程,算法与上述的RenderFile类似。

    现在我们知道了智能连接是怎么回事。DirectShow的这个机制,很“聪明”,可以方便地使用第三方(非Microsoft公司)开发的Filter。但是,有一个问题,就是如果系统中存在一些“恶意”的Filter,那么这个智能连接机制就会受到严峻的考验。因为这个原因,基于智能连接机制的应用程序(比如Windows Media Player)也会变得不稳定。(笔者并不赞成直接使用DirectShow Editing Services API进行非线性编辑,就是出于这方面的考虑。)
    大家可以下载我写的这个测试Filter源代码(http://hqtech.nease.net/Document.htm)。其实这是一个 CTransFormFilter的空架子,只是这个Filter的Merit值非常高(0x8800000),而且支持所有的Mediatype。注册这个Filter后,当有DirectShow应用程序使用智能连接机制时,就会反反复复地使用这个Filter进行试连接,没有休止。如果你使用 Windows Media Player播放媒体文件(AVI、MPEG、WMV等等),应用程序就会阻塞住;即使是RealOne Player,在播放微软格式的文件时也会出现这种现象。
    好了,不玩了,别把Media Player弄得太惨,毕竟对我们也没什么好处!:)期望大家已经对这个Merit有了更深的认识。接下去,把这个Filter从你的系统注销吧:regsvr32 /u yourlocalpath//HQMPKiller.ax。


    --  作者:admin
    --  发布时间:2005-11-26 2:56:00
    --  玩转 DVR-MS

    发布日期: 6/7/2005 | 更新日期: 6/7/2005

    Stephen Toub
    Microsoft Corporation

    适用于:
    Microsoft Windows XP Media Center Edition 2005
    Microsoft DirectShow
    DirectX 9.0 SDK

    摘要: Stephen Toub 讨论了 Windows XP Media Center 2005 生成的 DVR-MS 文件格式,介绍了 DirectShow 并展示了如何使用后者处理前者。

    下载 DVR-MS 示例 Code.msi

    本页内容


    播放 DVR-MS 文件


    DirectShow 和 GraphEdit 简介


    DirectShow 接口


    将编码转换为 WMV


    调试筛选器图形


    非托管资源清理


    将 WmvConverter 投入使用: WmvTranscoderPlugin


    访问 DVR-MS 元数据


    编辑 DVR-MS 文件


    小结


    相关书籍


    致谢

    几年前我拥有一台 TiVo。它已经不知藏在公寓壁橱的哪个角落了,我想现在一定是布满灰尘,诚然,就是现在我也可能这样对待它。占据电视旁宝贵空位的是一个更漂亮、更复杂的现代化软件和电子产品 — Microsoft Windows XP Media Center 2005。我的家人为该设备取了个既得体又人性化的名字 —“米老鼠”,它有许多神奇的功能。然而,当我建议我的“技术娴熟”的朋友们放弃他们现在使用的任一款数字摄像机 (DVR) 而转为使用此平台时,只要他们让我说明一个理由,我的回答都很简单:可以对录制的电视节目进行文件访问。

    DVR-MS 文件是由 Windows XP Service Pack 1 引入的流缓冲引擎(Stream Buffer Engine,SBE)创建的,Media Center 用它存储录制的电视节目。在本文中,我将向您演示如何通过托管代码使用 DirectShow 来处理和操作 DVR-MS 文件。在此过程中,我将向您介绍我为处理 DVR-MS 文件而创建的一些有用的实用工具,并为您提供您在编写自己的代码时需要的工具和库。所以,请打开 Visual Studio .NET,抓一把爆米花,享受这个过程吧。

    注 本文假定您的系统中有一个正在工作的 MPEG2 解码器,并且您使用的是 NTSC 而非 HD 内容(虽然这里讨论的大多数概念适用于 PAL 和 HD,但示例代码可能无法正确地处理这些格式)。另外,由于内容所有者或广播公司所设置的策略,一些 DVR-MS 文件受到复制保护。这种保护是在生成文件时通过检查广播公司的复制保护标志 (CGMS-A) 确定的,它会限制您访问特定 DVR-MS 文件的方式和时间。例如,在收费台(如 HBO)录制的电影可能是加密的,因此本文描述的技术就不适用了。最后,与本文相关联的代码示例和应用程序是针对 .NET Framework 1.1 编译的。然而,默认情况下 Windows XP Media Center 2005 并没有附带安装 .NET Framework 1.1,而是安装 1.0。因此,要在您的 Media Center 中使用这些示例,您必须安装 .NET Framework 1.1(可通过 Windows Update 获得)或者重新编译该示例以适用 .NET Framework 1.0。


    --  作者:admin
    --  发布时间:2005-11-26 2:56:00
    -- 

    播放 DVR-MS 文件

    谈到视频文件时,播放或许是可以执行的最重要的操作,所以我将从此入手。在您自己的应用程序中可以有多种播放 DVR-MS 文件的方式,这里我将演示其中的一些。为此,我创建了一个简单的应用程序(如图 1 所示),您可以在与本文有关的代码下载中获得。

    图 1. 播放 DVR-MS 文件的示例应用程序

    播放 DVR-MS 文件的第一种方式也是最简单的方式是,使用 System.Diagnostics.Process 类来执行它。由于 Process.Start 包装了来自 shell32.dll 的 ShellExecuteEx 非托管函数,因此这种方式利用了与从 Windows Explorer 双击一个文件相同的功能来播放 DVR-MS 文件:

    private void btnProcessStart_Click(object sender, System.EventArgs e){  Process.Start(txtDvrmsPath.Text);}

    这也意味着,视频将在一个独立的进程中播放,这个进程在 DVR-MS 文件的任何默认处理程序中运行;对于大多数机器和我的机器来说,它就是 Windows Media Player(我使用 Windows Media Player 10,如果您没有,我建议您从 http://www.microsoft.com/windows/windowsmedia/mp10/default.aspx 免费升级到该版本)。当然,Process.Start 有另一个同时接受可执行路径和参数的重载,可以使用它在任何您想要的播放机中启动 DVR-MS 文件,不管它是否是 .dvr-ms 扩展名的默认处理程序:

    private void btnProcessStart_Click(object sender, System.EventArgs e){    Process.Start(    @"c://Program Files//Windows Media Player//wmplayer.exe",    "//"" + txtDvrmsPath.Text + "//"");}

    您应该注意到,当这样做时,有必要对 DVR-MS 文件的路径加上引号(正如此处名为 txtDvrmsPath 的 TextBox 的内容所提供的),因为要使用的内容是 wmplayer.exe 的一个命令行参数。否则,路径中的任何空格都会使路径被分隔并解释为多个参数。

    Process.Start 返回一个代表启动进程的 Process 实例,这意味着您可以利用 Process 提供的功能来与 Windows Media Player 进一步交互。例如,在您的应用程序中,您可能想先等待视频停止再让用户继续,可以使用 Process.WaitForExit 方法来完成这样的任务:

    private void btnProcessStart_Click(object sender, System.EventArgs e){  using(Process p = Process.Start(txtDvrmsPath.Text))  {    p.WaitForExit();  }}

    当然,这只是等待 Media Player 关闭,而不是像初始请求那样播放您指定的文件,因为您的应用程序没有真正的视图可以查看 Media Player 执行的内容。当打开 Media Player 时,按上述方法编码也会冻结应用程序的 GUI,这个问题可以通过订阅 ProcessExited 事件加以解决,而不是用 WaitForExit 方法阻止。

    总而言之,该解决方案编码简单方便,但非常不灵活,而且是在应用程序的外部播放视频。它可能只在以下情况下才适用,您想允许用户查看指定的文件,不过是在应用程序不必关心视频内容而且应用程序根本不与视频交互的情况下查看。例如,如果您的应用程序是一个下载代理,而且您想允许用户查看已经复制到本地的视频文件,则可能适合采用这种方式。


    --  作者:admin
    --  发布时间:2005-11-26 2:57:00
    -- 

    由于我们知道 Windows Media Player 可以播放 DVR-MS 文件,因此对于大多数情况,更好的解决方案是在应用程序中宿主 Windows Media Player ActiveX 控件的一个实例。在 Visual Studio .NET 中,只需右键单击工具箱,选择添加控件并选择 Windows Media Player COM 控件。这样它就会出现在工具箱中,如图 2 所示。

    图 2. 工具箱中的 Windows Media Player ActiveX 控件

    当窗体中有一个 ActiveX 控件的实例时,让它播放 DVR-MS 文件就只需设置播放器的 URL 属性:

    player.URL = txtDvrmsPath.Text;

    在我的示例应用程序中,我选择让它更进一步。我创建了一个 System.Windows.Forms.Panel ,它位于想要显示视频的窗体中。当用户请求使用 Media Player 播放选定的视频时,我就新建一个 Media Player 控件的实例,将它添加到 Panel 的子控件集合中,使其保持在最大化,并设置其 URL 属性。这种方案允许我完全控制 Media Player 的生存期,而且可以轻松管理它在窗体中的位置,而不用担心它的绝对定位值(这种方案也使演示播放视频的其他方法变得轻松,稍后您将看到)。正在使用的这种方案的屏幕快照如图 3 所示,下面显示的是我使用的代码:

    private void btnWmp_Click(object sender, System.EventArgs e){  AxWindowsMediaPlayer player = new AxWindowsMediaPlayer();  pnlVideo.Controls.Add(player);  player.Dock = DockStyle.Fill;  player.PlayStateChange +=     new _WMPOCXEvents_PlayStateChangeEventHandler(      player_PlayStateChange);   player.URL = txtDvrmsPath.Text;}private void player_PlayStateChange(  object sender, _WMPOCXEvents_PlayStateChangeEvent e){  AxWindowsMediaPlayer player = (AxWindowsMediaPlayer)sender;  if (e.newState == (int)WMPLib.WMPPlayState.wmppsMediaEnded ||    e.newState == (int)WMPLib.WMPPlayState.wmppsStopped)  {    player.Parent = null; // removes the control from the panel    ThreadPool.QueueUserWorkItem(      new WaitCallback(CleanupVideo), sender);  }} private void CleanupVideo(object video){  ((IDisposable)video).Dispose();}

    图 3. 使用 WMP 控件的嵌入式 DVR-MS 播放

    要阻止显示 Media Player 工具栏,您可以更改控件的 uiMode 属性:

    player.uiMode = "none";

    要在用户右键单击控件时阻止显示 Media Player 上下文菜单,可以将其 enableContextMenu 属性设置为 false:

    player.enableContextMenu = false;

    您将注意到,在播放 DVR-MS 文件的前一刻,我为播放器的 PlayStateChange 事件注册了一个事件处理程序。这可以使我在播放停止时从 Panel 删除播放器。在 PlayStateChange 事件的处理程序中,我检查播放是否结束,如果结束,就将播放器从其父控件(面板)删除,并将一个工作项排入 .NET ThreadPool 队列中。这个工作项的作用只是处置播放器控件。我是在后台线程中进行此次处置的,因为无法在 PlayStateChange 事件处理程序中直接处置。在此事件处理程序中处置控件会在控件本身中引发异常,因为事件处理程序是在控件中引发的,控件在执行完我的处理程序之后还需要进行更多的处理。在处理程序中处置播放器控件会导致功能被破坏,所以我让该操作在事件处理程序完成之后稍微延迟一会,以便留出必要的时间。您将看到,在使用所演示的下一个播放机制时,就需要用到同一技术。

    宿主 Windows Media Player ActiveX 控件有许多好处。它使用起来非常方便,而且提供了大量的功能。然而,Windows Media Player 使用 DirectX(特别是 DirectShow)来播放 DVR-MS 文件(本文后面我将更详细地讨论 DirectShow)。您不是依赖 Windows Media Player 与 DirectX 交互,而是在您的应用程序中使用 Managed DirectX,完全跳过 Windows Media Player。

    在写作本文时 Managed DirectX 的最新版本是 DirectX 9.0 SDK Update February 2005 下载 的一部分。(要获得本文后面介绍的内容,您还需要 February 2005 Extras 下载 。)此 SDK 在您的全局程序集缓存 (GAC) 中安装了 AudioVideoPlayback.dll 程序集,使其可用于您的应用程序(DirectX 运行库安装也安装了此 DLL 以使您的最终用户可以访问它)。AudioVideoPlayback 是一个高级包装,它含有您在 .NET 应用程序中播放视频和音频文件所需要的最少的 DirectShow 功能。

    有了 Windows Media Player ActiveX 控件后,使用 AudioVideoPlayback 变得非常简单。

    private void btnManagedDirectX_Click(object sender, System.EventArgs e){  Video v = new Video(txtDvrmsPath.Text);  Size s = pnlVideo.Size;  v.Owner = pnlVideo;  v.Ending += new EventHandler(v_Ending);  v.Play();  pnlVideo.Size = s;}private void v_Ending(object sender, EventArgs e){  ThreadPool.QueueUserWorkItem(    new WaitCallback(CleanupVideo), sender);}private void CleanupVideo(object video){  ((IDisposable)video).Dispose();}

    这段代码首先实例化一个新的 Microsoft.DirectX.AudioVideoPlayback.Video 对象,然后将要播放的 DVR-MS 文件的路径提供给它。当播放一段 Video 时,它会自动将自身的大小(更具体地说是将它的所有者控件)调整为所播放视频的合适大小;为了解决这个问题,我存储了父面板控件的原始大小,这样在开始播放后就可以重置其大小。就像处理 ActiveX 控件那样,我注册了一个要在播放停止时激发的事件处理程序,然后播放视频。当播放结束时,我将一个工作项排入要处置 Video 对象的 ThreadPool 队列中,如同使用 ActiveX 控件一样(原因也相同)。当您不再使用 Video 对象时,对其进行处置是非常重要的;否则会浪费大量非托管资源,而且由于此对象有一个非常小的托管占地,垃圾回收器 (GC) 没有重大的动因可以及时进行回收,这样将使这些非托管资源的分配情况不明,除非您手动通过 IDisposable 处置。图 4 中的屏幕快照演示了 AudioVideoPlayback 功能的使用。

    图 4. 采用 AudioVideoPlayback 的嵌入式播放

    当然,虽然 AudioVideoPlayback 是一个高级 DirectShow 包装,但并不意味着您不能创建自己的托管包装(实际上,在本文后面我们将这样做)。创建托管包装的最简单方式是使用 tlbimp.exe(或者采用类似的做法 — 使用 Visual Studio .NET 的 COM 类型库导入功能。Visual Studio .NET 和 tlbimp.exe 都依赖于 Framework 中同样的库执行导入)。


    --  作者:admin
    --  发布时间:2005-11-26 2:57:00
    -- 

    DirectShow 运行库的核心库是 quartz.dll,位于 %windir%//system32//quartz.dll。它包含用于音频和视频播放的最重要的 COM 接口和 coclass,本文后面将对此进行更加详细的讨论。在 quartz.dll 上运行 tlbimp.exe 会产生一个 interop 库 — Interop.QuartzTypeLib.dll(此程序集的描述信息为“ActiveMovie control type library”,因为 DirectShow 的前身名为 ActiveMovie),并公开 FilgraphManagerClass (筛选器图形管理器)和 IVideoWindow 接口。要播放视频,您只需创建该图形管理器的一个新实例并使用 RenderFile 方法,在 DVR-MS 文件路径中传送,以便初始化该对象以进行播放。然后可以使用由 FilgraphManagerClass 实现的 IVideoWindow 接口来控制播放选项,例如所有者窗口、视频在父窗口中的位置,以及视频窗口的标题。要开始播放,可以使用 Run 方法。WaitForCompletion 方法可以用于等待视频停止播放(或者,可以指定一个正的毫秒数,作为要等待的最长时间),Stop 方法可以用于暂停播放。要销毁该对象并释放用于播放的所有非托管资源(包括播放窗口本身),System.Runtime.InteropServices.Marshal 类及其 ReleaseComObject 方法就会派得上用场了。使用 quartz.dll 的屏幕快照如图 5 所示。

    private void btnQuartz_Click(object sender, System.EventArgs e){  FilgraphManagerClass fm = new FilgraphManagerClass();  fm.RenderFile(txtDvrmsPath.Text);  IVideoWindow vid = (IVideoWindow)fm;  vid.Owner = pnlVideo.Handle.ToInt32();  vid.Caption = string.Empty;  vid.SetWindowPosition(0, 0, pnlVideo.Width, pnlVideo.Height);  ThreadPool.QueueUserWorkItem(new WaitCallback(RunQuartz), fm);}private void RunQuartz(object state){  FilgraphManagerClass fm = (FilgraphManagerClass)state;  fm.Run();  int code;  fm.WaitForCompletion(Timeout.Infinite, out code);  fm.Stop();  while(Marshal.ReleaseComObject(fm) > 0);}

    图 5. 使用 quartz.dll 的嵌入式播放

    我刚刚向您介绍了一些在自己的应用程序中播放 DVR-MS 文件的方法。虽然我讨论了多个播放 DVR-MS 文件的方法(而且我还没列举完),但所有这些方法都要依赖于 DirectShow 才有播放功能。因此,我们将简要介绍一下 DirectShow(或者让那些具有 DirectShow 经验的人重温一下)。

    返回页首

    DirectShow 和 GraphEdit 简介

    在本质上,使用 DirectShow 处理视频文件的应用程序是通过一组称为筛选器的组件完成的。一个筛选器通常只对多媒体数据流执行一种操作。这样的筛选器很多,每个筛选器执行不同的任务,例如读取 DVR-MS 文件、写出 AVI 文件、对 MPEG-2 压缩视频进行解码、将视频和音频呈现到视频卡和声卡上,等等。这些筛选器的实例可以连接在一起并组合成一个筛选器图形,然后由 DirectShow 筛选器图形管理器组件进行管理(在前面介绍 quartz.dll 时,您已简要地对其进行了了解)。这些图形是定向的,也是非循环的,这意味着两个筛选器之间的特定连接只允许数据朝一个方向流动,而且只能流经特定筛选器一次。这种数据流程称为流 (stream),而筛选器则用来处理这些流。筛选器是通过它们公开的针 (pin) 连接到其他筛选器的,因此,一个筛选器的输出针连接到另一个筛选器的输入针,并按从前者发送到后者的方式发送数据流。

    为了对此进行演示并显示本文中所使用的图形,我使用了 DirectX SDK 中一个名为 GraphEdit 的实用工具。GraphEdit 可以用来使筛选器图形可视化,当要确定如何构建用于特定目的的图形以及调试您所构建的图形时,这个功能就能派上用场。稍后,我将介绍如何使用 GraphEdit 来对在您的应用程序中运行的筛选器图形进行连接和可视化。

    现在,我们运行 GraphEdit。在“File”菜单下,选择“Render Media File”,然后选择本地可用的任何有效的 DVR-MS 文件(请注意,您可能需要在“Open File”对话框中将筛选器扩展名更改为“All Files”,而不是“All Media Files”,因为最近发布的 GraphEdit 版本并没有将 .dvr-ms 扩展名归类为媒体文件)。您应该能够看到一个图形,它类似于图 6 所示的图形。

    图 6. GraphEdit 准备播放 DVR-MS 文件

    此时,GraphEdit 已构造了一个筛选器图形,它能够播放选定的 DVR-MS 文件。这些蓝框中的每一个都是一个筛选器,箭头显示每个筛选器上的输入和输出针如何互相连接以形成图形。图形中的第一个筛选器是 StreamBufferSource 筛选器的实例,它由 Windows XP SP1 及更高版本的 %windir%//system32//sbe.dll 库公开。选择这个筛选器是因为它在注册表中配置为 .dvr-ms 扩展名的源筛选器 (HKCL//Media Type//Extensions//.dvr-ms//Source Filter)。它的作用是从磁盘中读取一个文件,并将该文件的数据以流的形式发送到图形的其他部分。它从一个 DVR-MS 文件提供三个流。


    --  作者:admin
    --  发布时间:2005-11-26 2:58:00
    -- 

    第一个是音频流。如果您检查第一个针的针属性(DVR Out - 1,可以通过右键单击 GraphEdit 中的针来访问针属性),您可以发现该针的主要类型是 Audio,而其子类型是 Encrypted/Tagged,这意味我们在对该数据进行任何操作之前必须先对它进行解密和/或取消标记。这个过程是由 Decrypter/Detagger 筛选器(由 %windir%//system32//encdec.dll 公开)处理的。Decrypter/Detagger 将加密/带标记的音频流作为输入,然后发出 MPEG-1 音频流(对于高清晰度的内容则输出 dolby-AC3 流),这一点您可以通过检查该筛选器的 In(Enc/Tag) 和 Out 针加以验证。这里将音频发送到 MPEG Audio Decoder 筛选器(由 quartz.dll 公开),通过它将音频解压缩为脉冲编码调制 (PCM) 音频流。音频流的最后一个筛选器 DirectSound Audio Renderer(也由 quartz.dll 公开)接收此 PCM 音频数据并在计算机的声卡上播放。

    DVR-MS 源筛选器提供的第二个流包含所录制的电视节目的闭合字幕数据。和音频流一样,闭合字幕流也是经过加密/标记的,所以它必须首先通过 Decrypter/Detagger 筛选器。如果查看此筛选器的 Out 针,您会发现其主要类型是 AUXLine21Data,而其子类型是 Line21_BytePair。电视节目中的闭合字幕是作为电视图像的一部分发送的,并专门编码到图像的 line 21 中。

    DVR-MS 源筛选器发出的第三个流是视频内容 (video feed)。与音频和闭合字幕数据一样,这个流也是经过加密/标记的,所以它必须首先通过 Decrypter/Detagger 筛选器。Decrypter/Detagger 筛选器的输出是 MPEG-2 视频流,所以它必须先通过 MPEG-2 视频解码器才能呈现视频。Microsoft 没有在 Windows 中附带 MPEG-2 解码器,所以系统中必须有可用的第三方解码器才能播放。解码后的视频流再送到默认的视频呈现程序(由 quartz.dll 公开)。

    单击图形上方的绿色播放按钮就会出现一个标题为 ActiveMovie Window 的新窗口并在该窗口中播放 DVR-MS 文件。请注意,由于闭合字幕 Decrypt/Tag Out 针没有连接到任何地方,因此在呈现视频时没有用到闭合字幕数据。您可以通过修改图形对此进行更改。实际做法是,首先删除默认的视频呈现程序(单击该筛选器并按“Delete”键),因为该呈现程序不能处理多路输入。具体来说,我们需要这样的呈现程序:它可以显示视频流,并能将包含呈现的闭合字幕数据的位图覆盖其上。如何从 Decrypter/Detagger 筛选器获取 line 21 字节对,将其作为位图呈现出来呢?Windows 实际上附带了一个正好可以完成此任务的 DirectShow 筛选器。使用“Graph”菜单下的“Insert Filters...”命令,展开树视图中的 DirectShow 筛选器节点并选择“Video Mixing Renderer 9”筛选器。单击“insert”按钮将此筛选器的实例添加到图形中,然后关闭“insert filters”对话框。现在,Video Mixing Renderer 9 筛选器成为图形的一部分了,但没有连接到任何地方,也就不能使用(实际上,如果您现在单击“play”按钮,则只播放音频,因为视频流没有连接到呈现程序)。单击 MPEG-2 解码器上的 Video Output 针,并将它拖到呈现程序的 VMR Input0 针上(请注意,如果您使用的解码器不是 NVDVD,则视频输出针的名称可能不同,但概念是一样的)。如果您现在播放图形,则会看到输出与使用默认视频呈现程序播放时基本一致。然而,您将看到,此时呈现程序筛选器公开了多个输入针(实际上,筛选器可以根据连接到它们的其他筛选器动态更改公开的针)。我们可以将闭合字幕 Decrypter/Detagger 筛选器的 Out 针连接到呈现程序的 VMR Input1 针上,以此利用这一特性。GraphEdit 会自动插入一个 Line 21 Decoder 2 筛选器,将 Decrypter/Detagger 筛选器连接到解码器筛选器,并将解码器筛选器连接到呈现程序筛选器。现在,您应该能看到如图 7 所示的图形。当您播放此图形时,您将看到闭合字幕像您期望的那样,以文本的形式出现在视频前。

    图 7. 将闭合字幕合并到视频显示中

    此时,对 DirectShow 不熟悉的读者可能会产生疑惑:是如何发现 Line 21 Decoder 2 筛选器的?为什么一开始只需使用 GraphEdit 的 Render Media File 操作就能构造出整个图形呢?GraphEdit 依赖 IGraphBuilder 接口提供的功能来查找和选择合适的筛选器,并在需要时将它们互连(IGraphBuilder 是由我们在介绍如何播放 DVR-MS 文件时简要提到的 FilgraphManager 组件实现的,实际上我们使用的 RenderFile 方法就是 IGraphBuilder 接口的一部分)。


    --  作者:admin
    --  发布时间:2005-11-26 2:58:00
    -- 

    这种用于自动构建筛选器图形的机制称为 Intelligent Connect。由于您并不真的需要知道 Intelligent Connect 的具体内容(除非您正在实现自己的筛选器并想让它们可以自动构建图形),因此在这里此主题我不想介绍得太多,而是让您参考 DirectX SDK 中该主题的详细文档。然而,简单地说,RenderFile 方法是一个简单的包装,它包装了 IGraphBuilder 中的另外两个方法:AddSourceFilterRenderRenderFile 首先调用 AddSourceFilter ,对于本地文件,它只需在注册表中查找正在播放的文件的扩展名所必需的源筛选器的类型,将适当的筛选器实例添加到筛选器图形中,并对它进行配置以使其指向指定的源文件。对于此源筛选器的每个输出针,RenderFile 再调用 Render 方法,该方法试图查找从此针到图形中的呈现程序的一条路径。如果该针实现了 IStreamBuilder 接口,则 Render 只是委托该实现,将所有细节都交给该筛选器的实现。否则,Render 会试图查找此针可以连接的筛选器。为此,它会查找在图形构建过程前期可能缓存的缓存筛选器,查找已经成为图形的一部分且有未连接的输入针的任何筛选器,并使用 IFilterMapper 接口查找注册表中兼容的筛选器类型。如果找到了一个筛选器,则它会再对这个新的筛选器重复此过程,直到到达呈现筛选器,此时就成功地停止。如果没有找到筛选器,则 Intelligent Connect 构建图形未成功。这就是依赖 Intelligent Connect 的一个缺点:它并非始终有效。另外,如果您的机器上安装了新的筛选器,则 Intelligent Connect 可能会选择这些新的筛选器,而不是您当前期望在应用程序中使用的筛选器。因此,您在设计时可能要选择避免这种情况(我后面将要介绍,如果您确切地知道想在图形中使用哪些筛选器,则显式构建图形而不使用 Intelligent Connect 是很容易的)。

    既然您对 DirectShow 已有所了解,我们将要以编程方式使用它,以便对 DVR-MS 文件进行许多很合适的操作。毕竟,一旦 DVR-MS 源筛选器加载到图形中,我们就可以像处理其他音频和视频数据流那样处理来自 DVR-MS 的数据,操作它们的方法是无限的。


    --  作者:admin
    --  发布时间:2005-11-26 2:58:00
    -- 

    DirectShow 接口

    然而,我们首先需要的是能够以编程方式处理 DirectShow。对于非托管代码,这可能是立即可行的,因为 SDK 包含了通过 C++ 访问 DirectShow 库所需要的所有头文件。对于托管代码,问题就有些棘手。虽然 Managed DirectX 确实包含前面讨论的 AudioVideoPlayback.dll 库,但该库级别很高,它提供 VideoAudio 级别的抽象,而我们需要的是能够在筛选器和针级别对筛选器图形进行操作。虽然我觉得这个问题将来会得到改善,但至少当前版本的 Managed DirectX 对我们爱莫能助。

    quartz.dll 是什么?quartz.dll 的类型库公开了一些我们需要的功能,这里列出所公开接口的完整列表

    [此贴子已经被作者于 2005-11-26 3:03:44编辑过]


    --  作者:admin
    --  发布时间:2005-11-26 2:59:00
    -- 

    接口
    描述

    IAMCollection

    筛选器图形对象集合,例如筛选器或针。

    IAMStats

    允许应用程序从图形管理器中检索性能数据。筛选器可以使用此接口记录性能数据。

    IBasicAudio

    允许应用程序控制音频流的音量和平衡。

    IBasicVideo

    允许应用程序设置视频属性,例如目标矩形和源矩形

    IBasicVideo2

    从 IBasicVideo 接口派生,为应用程序提供了一个附加方法,通过它可以检索视频流的首选纵横比。

    IDeferredCommand

    允许应用程序取消或修改该应用程序先前使用 IQueueCommand 接口排入队列的图形-控制命令。

    IFilterInfo

    管理筛选器的信息并提供访问筛选器和表示筛选器上的针的 IPinInfo 接口。

    IMediaControl

    提供方法来控制经过筛选器图形的数据流。它包含运行、暂停和停止图形的方法。

    IMediaEvent

    包含用来检索事件通知和用于重写筛选器图形管理器的默认事件处理的方法。

    IMediaEventEx

    从 IMediaEvent 派生并添加方法来启用一个应用程序窗口,以便在事件发生时接收消息。

    IMediaPosition

    包含用于查找流中一个位置的方法。

    IMediaTypeInfo

    包含用于检索针连接的媒体类型的方法。

    IPinInfo

    包含用于检索针信息和连接针的方法。

    IQueueCommand

    允许应用程序预先将图形-控制命令排入队列。

    IRegFilterInfo

    提供对 Windows 注册表中的筛选器的访问,以及向筛选器图形中添加已注册的筛选器。

    IVideoWindow

    包含用于设置窗口所有者、窗口的位置和尺寸及其他窗口属性的方法。

    [此贴子已经被作者于 2005-11-26 3:12:42编辑过]


    --  作者:admin
    --  发布时间:2005-11-26 3:13:00
    --  
    这确实是个很好的开头,但它没有为我们提供一些处理图形和筛选器的最重要的接口。例如,手动构造图形比较常用的接口之一,IGraphBuilder 接口,并没有包括在内。表示特定筛选器实例和提供对其针访问的 IBaseFilter 接口也没有包括在内。下表列出了在本文中要完成图形需要访问的主要接口:


    --  作者:admin
    --  发布时间:2005-11-26 3:15:00
    -- 

    接口
    描述

    IBaseFilter

    提供用于控制筛选器的方法。应用程序可以使用此接口枚举针和查询筛选器信息。

    IConfigAsfWriter2

    提供用于获取和设置 WM ASF Writer 筛选器写文件要使用的高级流格式(Advanced Streaming Format,ASF)配置文件的方法和用于支持 Windows Media Format 9 Series SDK 中的新功能(例如双向编码和对反交错视频的支持)的方法。

    IFileSinkFilter

    在将媒体流写入文件的筛选器上实现。

    IFileSourceFilter

    在从文件读媒体流的筛选器上实现。

    IGraphBuilder

    提供方法来支持应用程序构建筛选器图形。

    IMediaControl

    提供方法来控制数据流经筛选器图形的流程。它包括用于运行、暂停和停止图形的方法。

    IMediaEvent

    包含用于检索事件通知和重写筛选器图形管理器的默认事件处理的方法。

    IMediaSeeking

    包含用于查询当前位置和查找流中的特定位置的方法。

    IWmProfileManager

    用于创建配置文件、加载现有的配置文件和保存配置文件。

    另外,我还需要显式实例化各个 COM 类,下面展示了其中最重要的一些类,以及它们的类 ID 和对每个类的描述:


    类 ID
    描述

    筛选器图形管理器

    E436EBB3-524F-11CE-9F53-0020AF0BA770

    构建和控制筛选器图形。此对象是 DirectShow 中的中心组件。

    Decrypter/Detagger 筛选器

    C4C4C4F2-0049-4E2B-98FB-9537F6CE516D

    有条件地解密由 Encrypter/Tagger 筛选器加密的示例。输出类型与 Encrypter/Tagger 筛选器接收到的原始输入类型相匹配。

    WM ASF Writer 筛选器

    7C23220E-55BB-11D3-8B16-00C04FB6BD3D

    接受数量可变的输入流并创建高级流格式 (ASF) 文件。


    --  作者:admin
    --  发布时间:2005-11-26 3:16:00
    -- 

    正如 Eric Gunnerson 在关于 DirectShow 和 C# 的 his blog entry 中指出的,一种快捷简便的导入接口的方法是使用 DirectX SDK 附带的 DirectShow 接口定义语言(Interface Definition Language,IDL)文件。这些文件包含了 COM 接口定义,我对其中的大部分接口都很感兴趣。我可以创建自己的 IDL 文件(它的创作是为了产生一个类型库),然后通过 Microsoft 接口定义语言 (MIDL) 编译器 (midl.exe) 运行它。这将产生一个类型库,然后我再使用 .NET Framework tool Type Library Importer (tlbimp.exe) 将它转换成托管程序集。

    遗憾的是,Eric 也指出,它不是一个完美的解决方案。首先,随 DirectX SDK 附带的 IDL 文件并没有描述我需要的所有接口,例如 IMediaEventIMediaControl 。其次,即使我需要的所有接口都描述了,但通常需要对 interop 签名的创建进行更多控制,而不只是 tlbimp.exe 所提供的控制。例如,如果在图形运行完成之前用户指定的时间到期,则 IMediaEvent.WaitForCompletion (本文后面将会介绍)会返回一个 E_ABORT HRESULT;它将转换成在 .NET 中引发的异常,如果您在轮询循环中要频繁调用 WaitForCompletion (我就打算这样做),则这样做就不合适。另外,IDL 类型和托管类型之间并不是一对一的映射;实际上,存在这样的情况,类型可能根据使用它的上下文不同而进行不同的封送处理。例如,在 DirectX SDK 的 axcore.idl 文件中,IEnumPins 接口公开了以下方法:

    HRESULT Next(  [in] ULONG cPins,            // Retrieve this many pins.  [out, size_is(cPins)] IPin ** ppPins,  // Put them in this array.  [out] ULONG * pcFetched         // How many were returned?);

    当它编译成类型库并由 tlbimp.exe 进行转换时,产生的程序集包含以下方法:

    void Next(  [In] uint cPins,   [Out, MarshalAs(UnmanagedType.Interface)] out IPin ppPins,   [Out] out uint pcFetched);

    虽然非托管的 IEnumPins::Next 可以被任何正整数值的 cPins 调用,但如果调用托管版本用的 cPins 值不是 1,则会产生错误,因为 ppPins 不是 IPin 实例数组,而是单个 IPin 实例的引用。


    --  作者:admin
    --  发布时间:2005-11-26 3:16:00
    -- 

    基于所有这些原因,以及 DirectShow 接口相对简单,我选择手动用 C# 实现 COM 接口 interop 定义;虽然这需要的工作更多,但它可以让您最好地控制封送内容、方式和时间(不过,请注意,在创建这些手动编码的 interop 定义时,采用 tlbimp.exe 生成的 MSIL 是一个很好的起点,或者更好的方式 — 采用这些导入类型库的反编译 C# 实现,可以使用 Lutz Roeder 的 .NET 发送程序生成它,这个程序可以从 http://www.aisto.com/roeder/dotnet/ 获得)。在与本文有关的代码下载中,您会发现我在本文中使用的每个非托管 DirectShow 接口都有手动编码的 C# 接口。举个例子,下面是前面讨论的 IGraphBuilder 接口的 C# 实现:

    [ComImport][Guid("56A868A9-0AD4-11CE-B03A-0020AF0BA770")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IGraphBuilder{  void AddFilter([In] IBaseFilter pFilter,     [In, MarshalAs(UnmanagedType.LPWStr)] string pName);  void RemoveFilter([In] IBaseFilter pFilter);  IEnumFilters EnumFilters();  IBaseFilter FindFilterByName(    [In, MarshalAs(UnmanagedType.LPWStr)] string pName);  void ConnectDirect([In] IPin ppinOut, [In] IPin ppinIn,     [In, MarshalAs(UnmanagedType.LPStruct)] AmMediaType pmt);  void Reconnect([In] IPin ppin);  void Disconnect([In] IPin ppin);  void SetDefaultSyncSource();  void Connect([In] IPin ppinOut, [In] IPin ppinIn);  void Render([In] IPin ppinOut);  void RenderFile(    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFile,    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrPlayList);  IBaseFilter AddSourceFilter(    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFileName,    [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFilterName);  void SetLogFile(IntPtr hFile);  void Abort();  void ShouldOperationContinue();}

    然后就可以通过我的 IGraphBuilder 接口来转换和使用筛选器图形管理器组件的实例。那么,如何获取筛选器图形管理器组件的实例呢?我使用了如下代码:

    public class ClassId{  public static readonly Guid FilterGraph =     new Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770");  public static readonly Guid WMAsfWriter =     new Guid("7C23220E-55BB-11D3-8B16-00C04FB6BD3D");  public static readonly Guid DecryptTag =     new Guid("C4C4C4F2-0049-4E2B-98FB-9537F6CE516D");  ...  public static object CoCreateInstance(Guid id)  {    return Activator.CreateInstance(Type.GetTypeFromCLSID(id));  }}

    在这个包装就位后,我就可以创建筛选器图形管理器的实例,配置能够播放 DVR-MS 文件的筛选器图形,以及播放文件,总共只需要五行代码:

    object filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);((IGraphBuilder)filterGraph).RenderFile(pathToDvrmsFile);((IMediaControl)filterGraph).Run();EventCode status;((IMediaEvent)filterGraph).WaitForCompletion(Timeout.Infinite, out status);

    既然我们知道如何通过托管代码使用 DirectShow,现在我们就来看看如何利用它做一些很酷的事情。


    --  作者:admin
    --  发布时间:2005-11-26 3:17:00
    -- 

    将编码转换为 WMV

    如果 Internet 搜索引擎能提供任何线索,则人们对 DVR-MS 文件想做的最流行的一件事情就是将它们转换成 Windows Media Video 文件。通过目前我们为处理 DVR-MS 文件和 DirectShow 而创建的框架,这个任务是很容易实现的。简单地说,我需要做的就是创建一个使用 DVR-MS 源筛选器和 WM ASF Writer 筛选器接收器(它编码并写出 WMV 文件)的图形,并在它们之间建立适当的筛选器和连接。我故意对这些中间筛选器含糊其词,因为我可以让 Intelligent Connect 替我查找和插入它们。作为说明手动进行此操作的简单性的例子,我们按照以下简单步骤在 GraphEdit 中创建适当的转换图形:

    1.

    打开 GraphEdit。

    2.

    从“Graph”菜单中选择“Insert Filters”,插入一个 DirectShow WM ASF Writer 筛选器。当提示输入一个输出文件名时,请输入目标文件的名称,以 .wmv 为扩展名。

    3.

    从“File”菜单中选择“Render Media File”,并在弹出的“Open File”对话框中选择输入的 DVR-MS 文件(再次提醒,您很可能需要将筛选器文件扩展名更改为“All Files”而不是“All Media Files”)。

    GraphEdit 将使用该图形的 RenderFile 方法来为 DVR-MS 文件添加一个源筛选器,并通过需要的一系列中间筛选器将它连接到适当的呈现程序。由于以上操作发生时 WM ASF Writer 筛选器接收器已经在图形中,因此使用 Intelligent Connect 的 RenderFile 会将流发送到该筛选器接收器上,而不是插入新的默认呈现程序筛选器。您应该能看到如图 8 所示的图形。

    图 8. 将 DVR-MS 编码转换为 WMV 的图形

    以编程方式进行这种转换是非常简单的,可以通过以下代码实现:

    // Get the filter graphobject filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);DisposalCleanup.Add(filterGraph);IGraphBuilder graph = (IGraphBuilder)filterGraph;// Add the ASF writer and set the output nameIBaseFilter asfWriterFilter = (IBaseFilter)  ClassId.CoCreateInstance(ClassId.WMAsfWriter);DisposalCleanup.Add(asfWriterFilter);graph.AddFilter(asfWriterFilter, null);IFileSinkFilter sinkFilter = (IFileSinkFilter)asfWriterFilter;sinkFilter.SetFileName(OutputFilePath, null);// Render the DVR-MS file and run the graphgraph.RenderFile(InputFilePath, null);RunGraph(graph, asfWriterFilter);

    先创建一个筛选器图形,将 WM ASF Writer 筛选器添加到其中并配置为指向适当的输出文件路径,然后将 DVR-MS 文件添加到该图形中并使用图形的 RenderFile 方法来呈现。遗憾的是,这在控制 WMV 文件编码方式上并没有提供很多灵活性。为了做到这一点,我们需要用一个配置文件配置 WM ASF Writer,这可以通过在调用 RenderFile 之前插入以下代码来完成:

    // Set the profile to be used for conversionif (_profilePath != null){  // Load the profile XML contents  string profileData;  using(StreamReader reader =     new StreamReader(File.OpenRead(_profilePath)))  {    profileData = reader.ReadToEnd();  }  // Create an appropriate IWMProfile from the data  IWMProfileManager profileManager = ProfileManager.CreateInstance();  DisposalCleanup.Add(profileManager);  IntPtr wmProfile = profileManager.LoadProfileByData(profileData);  DisposalCleanup.Add(wmProfile);  // Set the profile on the writer  IConfigAsfWriter2 configWriter =    (IConfigAsfWriter2)asfWriterFilter;  configWriter.ConfigureFilterUsingProfile(wmProfile); }

    这段代码假定配置文件 PRX 文件的路径已经存储在字符串成员变量 _profilePath 中。首先,使用 System.IO.StreamReader 将该配置文件的 XML 内容读到一个字符串中。然后创建 Windows Media Profile Manager(通过 IWMProfileManager 接口访问),并使用该管理器的 LoadProfileByData 方法将配置文件加载到其中。这为我们提供了一个指向所加载的配置文件的接口指针,可以用它来配置 WM ASF Writer 筛选器。WM ASF Writer 筛选器实现了 IConfigAsfWriter2 接口,它提供了 ConfigureFilterUsingProfile 方法,这个方法可以根据接口指针指定的配置文件配置编写器。

    创建和配置好图形之后,剩下的工作就是运行它,我是使用特意指定的 RunGraph 方法实现的。该方法首先获取指定图形的 IMediaControlIMediaEvent 接口。它还试图获取可用于跟踪源 DVR-MS 文件处理进度的 IMediaSeeking 接口。然后使用 IMediaControl 接口来运行图形,从此时开始,方法中的剩余代码仅仅是用来跟踪转换的处理进度。在图形结束运行前,代码会不断轮询 IMediaEvent.WaitForCompletion 方法,如果等待时间已到但图形还没完成运行,则该方法将返回状态代码 EventCode.None (0x0)。如果发生这种情况,则会使用 IMediaSeeking 接口来查询已经处理多少 DVR-MS 文件以及该文件的持续时间,由此我可以计算文件处理的百分比。

    当图形最终完成运行时,IMediaEvent.WaitForCompletion 会返回 EventCode.Complete (0x1),并使用 IMediaControl.Stop 来停止图形。


    --  作者:admin
    --  发布时间:2005-11-26 3:17:00
    -- 

    protected void RunGraph( IGraphBuilder graphBuilder, IBaseFilter seekableFilter){ IMediaControl mediaControl = (IMediaControl)graphBuilder; IMediaEvent mediaEvent = (IMediaEvent)graphBuilder; IMediaSeeking mediaSeeking = seekableFilter as IMediaSeeking; if (!CanGetPositionAndDuration(mediaSeeking))  {  mediaSeeking = graphBuilder as IMediaSeeking;  if (!CanGetPositionAndDuration(mediaSeeking)) mediaSeeking = null; } using(new GraphPublisher(graphBuilder,  Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf")) {  mediaControl.Run();  try  {   OnProgressChanged(0);   bool done = false;   while(!CancellationPending && !done)   {    EventCode statusCode = EventCode.None;    int hr = mediaEvent.WaitForCompletion(     PollFrequency, out statusCode);    switch(statusCode)    {     case EventCode.Complete:      done = true;      break;     case EventCode.None:       if (mediaSeeking != null)      {       ulong curPos = mediaSeeking.GetCurrentPosition();       ulong length = mediaSeeking.GetDuration();       double progress = curPos * 100.0 / (double)length;       if (progress > 0) OnProgressChanged(progress);      }      break;     default:      throw new DirectShowException(hr, null);    }   }   OnProgressChanged(100);  }  finally { mediaControl.Stop(); } }}

    简单吧?DirectShow 是一项令人惊讶的技术。这段代码允许您将非 DRM/'d、NTSC、存储在 DVR-MS 文件中的 SD 内容转换成 WMV 文件。如果您检查本文代码下载中的文件,正如您将看到的,我已将此函数编码到一个名为 Converter 的抽象基类中。一个派生类(在本例中为 WmvConverter )构建合适的图形,然后调用基类的 RunGraph 方法。另外,Converter 还公开了可用于配置、监视和暂停图形流程的属性和事件,正如您在以下部分将看到的,Converter 公开了使调试图形变得更加简单的功能。


    --  作者:admin
    --  发布时间:2005-11-26 3:19:00
    -- 

    调试筛选器图形

    您将在 RunGraph 方法中看到,图形是在如下所示的 using 块内部运行的:

    using(new GraphPublisher(graphBuilder,  Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf")){  ... // run the graph}

    我这里使用的 GraphPublisher 类是一个自定义类,它是我为帮助调试图形而编写的。它有两个用途。第一,如果在 GraphPublisher 的构造函数的第二个参数中指定了一个文件路径,则它会将 graphBuilder 对象所表示的图形保存到该文件中(该文件应该使用 .grf 扩展名)。随后 GraphEdit 可以打开此文件,从而让您查看整个图形,如同它在发布时出现的样子。这个功能可以通过筛选器图形管理器的 IPersistStream 接口实现来使用:

    private const ulong STGM_CREATE = 0x00001000L;private const ulong STGM_TRANSACTED = 0x00010000L;private const ulong STGM_WRITE = 0x00000001L;private const ulong STGM_READWRITE = 0x00000002L;private const ulong STGM_SHARE_EXCLUSIVE = 0x00000010L;[DllImport("ole32.dll", PreserveSig=false)]private static extern IStorage StgCreateDocfile(  [MarshalAs(UnmanagedType.LPWStr)]string pwcsName,   [In] uint grfMode, [In] uint reserved);private static void SaveGraphToFile(IGraphBuilder graph, string path){  using(DisposalCleanup dc = new DisposalCleanup())  {    string streamName = "ActiveMovieGraph";    IPersistStream ps = (IPersistStream)graph;    IStorage graphStorage = StgCreateDocfile(path,      (uint)(STGM_CREATE | STGM_TRANSACTED |       STGM_READWRITE | STGM_SHARE_EXCLUSIVE), 0);    dc.Add(graphStorage);    UCOMIStream stream = graphStorage.CreateStream(      streamName, (uint)(STGM_WRITE | STGM_CREATE |       STGM_SHARE_EXCLUSIVE), 0, 0);    dc.Add(stream);    ps.Save(stream, true);    graphStorage.Commit(0);  }}

    然而,GraphPublisher 的主要目的和它在 using 块中使用的原因是将实时图形发布到 GraphEdit。GraphEdit 允许您连接到另一个流程所公开的远程图形,只要该图形已经发布到运行中对象表 (ROT) — 一个用作跟踪运行对象的全局可访问的查找表。GraphEdit 不仅可以让您在另一个流程中查看和检查一个实时筛选器图形,它还常常允许您对其加以控制。

    该图形发布到 ROT 是使用以下代码完成的:

    private class RunningObjectTableCookie : IDisposable{  private int _value;  private bool _valid;  internal RunningObjectTableCookie(int value)  {    _value = value;    _valid = true;  }  ~RunningObjectTableCookie() { Dispose(false); }  public void Dispose()  {    GC.SuppressFinalize(this);    Dispose(true);  }  private void Dispose(bool disposing)  {    if (_valid)    {      RemoveGraphFromRot(this);      _valid = false;      _value = -1;    }  }  internal bool IsValid   {     get { return _valid; } set { _valid = value; }   }}private static RunningObjectTableCookie AddGraphToRot(  IGraphBuilder graph){  if (graph == null) throw new ArgumentNullException("graph");  UCOMIRunningObjectTable rot = null;  UCOMIMoniker moniker = null;  try   {    // Get the ROT    rot = GetRunningObjectTable(0);    // Create a moniker for the graph    int pid;    using(Process p = Process.GetCurrentProcess()) pid = p.Id;    IntPtr unkPtr = Marshal.GetIUnknownForObject(graph);    string item = string.Format("FilterGraph {0} pid {1}",       ((int)unkPtr).ToString("x8"), pid.ToString("x8"));    Marshal.Release(unkPtr);    moniker = CreateItemMoniker("!", item);        // Registers the graph in the running object table    int cookieValue;    rot.Register(ROTFLAGS_REGISTRATIONKEEPSALIVE, graph,       moniker, out cookieValue);    return new RunningObjectTableCookie(cookieValue);  }  finally  {    // Releases the COM objects    if (moniker != null)       while(Marshal.ReleaseComObject(moniker)>0);     if (rot != null) while(Marshal.ReleaseComObject(rot)>0);   }}private static void RemoveGraphFromRot(RunningObjectTableCookie cookie){  if (!cookie.IsValid) throw new ArgumentException("cookie");  UCOMIRunningObjectTable rot = null;  try   {    // Get the running object table and revoke the cookie    rot = GetRunningObjectTable(0);    rot.Revoke(cookie.Value);    cookie.IsValid = false;  }  finally  {    if (rot != null) while(Marshal.ReleaseComObject(rot)>0);   }}private const int ROTFLAGS_REGISTRATIONKEEPSALIVE  = 1;[DllImport("ole32.dll", ExactSpelling=true, PreserveSig=false)]private static extern UCOMIRunningObjectTable GetRunningObjectTable(  int reserved);[DllImport("ole32.dll", CharSet=CharSet.Unicode,   ExactSpelling=true, PreserveSig=false)]private static extern UCOMIMoniker CreateItemMoniker(  [In] string lpszDelim, [In] string lpszItem);

    在其构造函数中,GraphPublisher 使用 AddGraphToRot 将图形添加到 ROT 中,并存储产生的 cookie。在其 IDisposable.Dispose 方法中,GraphPublisher 通过将存储的 cookie 传递到 RemoveGraphFromRot 来将图形从 ROT 中删除。


    --  作者:admin
    --  发布时间:2005-11-26 3:21:00
    -- 

    非托管资源清理

    当资源使用完毕后,尽早将它们释放是非常重要的。当使用处理大量音频和视频资源的 DirectShow COM 对象时,这一点尤其重要。可以使用 Marshal.ReleaseComObject 方法来强制处置 COM 对象,此方法会减少所提供的运行时可调用包装的引用计数。当引用数到达零时,运行库会释放它在非托管 COM 对象上的所有引用。(有关 Marshal.ReleaseComObject 的更多信息,请参见该方法的 MSDN 文档 。)对于使用的每个 COM 对象,我不是将我的代码随便放在 try/finally 块中,而是创建一个名为 DisposalCleanup 的助手类,它可以简化 COM 对象的生存期管理:

    public class DisposalCleanup : IDisposable{  private ArrayList _toDispose = new ArrayList();  public void Add(params object [] toDispose)  {    if (_toDispose == null)       throw new ObjectDisposedException(GetType().Name);    if (toDispose != null)    {      foreach(object obj in toDispose)      {        if (obj != null && (obj is IDisposable ||           obj.GetType().IsCOMObject || obj is IntPtr))        {          _toDispose.Add(obj);        }      }    }  }  void IDisposable.Dispose()  {    if (_toDispose != null)    {      foreach(object obj in _toDispose) EnsureCleanup(obj);      _toDispose = null;    }  }  private void EnsureCleanup(object toDispose)  {    if (toDispose is IDisposable)     {      ((IDisposable)toDispose).Dispose();    }    else if (toDispose is IntPtr) // IntPtrs must be interface ptrs    {      Marshal.Release((IntPtr)toDispose);    }    else if (toDispose.GetType().IsCOMObject)     {      while (Marshal.ReleaseComObject(toDispose) > 0);    }  }}

    这里一个重要的方法是 EnsureCleanup ,它是通过 DisposalCleanupIDisposable.Dispose 方法调用的。通过使用其 Add 方法来调用添加到 DisposalCleanup 中的每个对象,EnsureCleanup 调用了一个 IDisposable 对象上的 Dispose 、一个 COM 对象上的 Marshal.ReleaseComObject 和一个接口指针上的 Marshal.Release 。通过这些,我的代码只需将使用许多 COM 对象的代码块放在一个创建了新的 DisposalCleanup 的 using 块中,将任何 COM 对象或接口添加到 DisposalCleanup 实例中,并在 using 块结束时调用 DisposalCleanupIDisposable.Dispose 方法来释放所有使用过的资源。我的 Converter 基类实现了此方案,并通过一个受保护的 DisposalCleanup 属性公开了构造的 DisposalCleanup

    public object Convert() {   _cancellationPending = false;  try  {    object result;    using(_dc = new DisposalCleanup())    {      // Do the actual work      result = DoWork.();    }    OnConversionComplete(null, result);    return result;  }  catch(DirectShowException exc)  {    OnConversionComplete(exc, null);    throw;  }  catch(Exception exc)  {    exc = new DirectShowException(exc);    OnConversionComplete(exc, null);    throw exc;  }  catch  {    OnConversionComplete(new DirectShowException(), null);    throw;  }}private DisposalCleanup _dc;protected DisposalCleanup DisposalCleanup { get { return _dc; } }

    DoWork. 方法是抽象方法,如果是 WmvConverter 类,它可以构建筛选器图形并调用 RunGraph 方法。通过这种方式,派生类可以实现 DoWork. 并简单地向基类的 DisposalCleanup 中添加可处置的对象;当派生类的工作执行完毕后,即使它引发异常,基类也会自动处置这些资源。


    --  作者:admin
    --  发布时间:2005-11-26 3:21:00
    -- 

    将 WmvConverter 投入使用: WmvTranscoderPlugin

    显而易见,通过前面讨论的代码,您可以编写功能丰富的应用程序来处理 DVR-MS 文件并将其转换成 WMV 文件。但据我所见,此功能最常见的请求是作为 Media Center-集成解决方案的一部分。由此创建了许多非常有用的解决方案,其中最著名的有 Dan Giambalvo 创建的 dCut (可通过 http://www.inseattle.org/~dan/Dcut.htm 下载)以及 Alex Seigler、José Peña、James Edelen 和 Jeff Griffin 创建的 DVR 2 WMV(可通过 http://www.thegreenbutton.com/downloads.aspx 下载)。这两个应用程序都依赖于 Alex Siegler 编写的 dvr2wmv DLL(使用的技术与本文所介绍的非常类似,不过采用的是非托管代码)。这些应用程序不懈努力地尝试集成到 Media Center 中,更具体地说是模仿 Media Center 外壳的外观,但遗憾的是,目前的 Media Center SDK 只允许做到这么多。幸运的是,SDK 有另一个相对未开发的区域,它使这种功能可以轻松地集成到 Media Center UI 中,但仍然保留 Media Center 团队已编写的所有烙印:ListMaker 外接程序。

    ListMaker 外接程序是由第三方提供的托管组件,它运行在 Media Center 进程内,使用 Microsoft.MediaCenter.dll 程序集公开的 API 元素(您可以在 Media Center 系统的 %windir%//ehome 目录下找到此 DLL)。ListMaker 外接程序的工作非常简单:它的目的是获取 Media Center 提供给它的文件列表,并对该列表进行一些操作(进行什么操作取决于该外接程序)。Media Center 已将它构建到 UI 中以处理列表生成和随外接程序处理列表时的报告而显示的进程更新。很酷的一点是 Media Center 并不在意该外接程序对媒体列表进行了什么操作。因此,您可以编写这样一个外接程序,它将用户选定的每个 DVR-MS 文件转换成 WMV,并将它们写到硬盘的一个文件夹中。更明确地说,我拥有这样的外接程序(图 9),下面我将向您介绍如何实现。

    图 9. WMV Transcoder 外接程序

    首先,ListMaker 外接程序必须从 System.MarshalByRefObject 派生,如同所有用于 Media Center 的外接程序那样(遗憾的是,SDK 文档目前没有提到这一点,但是这一点非常重要)。Media Center 将所有外接程序加载到一个独立的应用程序域中,这意味着它使用 .NET Remoting 基础结构跨应用程序域边界访问该外接程序。MarshalByRefObject 类能实现这一目的,它允许跨应用程序域边界访问对象,因此外接程序必须以它为基类。如果您忘记从 MarshalByRefObject 派生,则您的外接程序将无法正确加载或运行。

    除了从 MarshalByRefObject 派生外,ListMaker 外接程序还实现了两个来自 Microsoft.MediaCenter.dll 程序集的主要接口:Microsoft.MediaCenter.AddIn.IAddInModuleMicrosoft.MediaCenter.AddIn.ListMaker.ListMaker

    public class WmvTranscoderPlugin : MarshalByRefObject,   IAddInModule, ListMakerApp, IBrandInfo{  ...}

    所有 Media Center 外接程序都实现了 IAddInModuleIAddInModule 通过实现 IAddInModule.InitializeIAddInModule.Uninitialize 方法来初始化和处置要运行的代码。在许多情况下,初始化阶段需要做的事情非常少;对于我的外接程序,我只需查看一下注册表,找到用户首选项,例如经过编码转换的文件应该写到哪个磁盘(注册表中 HKLM//Software//Toub//WmvTranscoderPlugin 项的 PreferredDrive 值)以及应该使用哪个 Windows Media 配置文件来将代码转换为 WMV(注册表中的 HKLM//Software//Toub//WmvTranscoderPlugin 项的 ProfilePath 值)。如果没有指定驱动器(或者指定的驱动器无效),则我将默认值设置为从 System.IO.Directory.GetLogicalDrives 返回的第一个有效的驱动器,其中,有效的驱动器定义为 Win32 GetDriveType 函数声明的固定驱动器中的任何一个驱动器。

    ListMakerApp 是列表的主要接口,用于处理和服务双重目的:允许用户选择要处理的媒体文件集(图 10)并启动外接程序的处理,在这之后它允许 Media Center UI 报告进度(图 11)。

    图 10. 选择要进行编码转换的节目

    图 11. Media Center 外壳中的进度更新

    前者涉及的成员并不令人非常满意,所以我不想花太多时间介绍它们。从根本上说,Media Center 通过此接口调用外接程序以获取如选择多少 DVR-MS 文件、还能添加多少文件之类的信息,并在每次用户更改要处理的列表项时调用它。它的核心部分是由三个方法处理的:

    public void ItemAdded(ListMakerItem item){  _itemsUsed++;  _bytesUsed += item.ByteSize;  _timeUsed += item.Duration;}public void ItemRemoved(ListMakerItem item){  _itemsUsed--;  _bytesUsed -= item.ByteSize;  _timeUsed -= item.Duration;}public void RemoveAllItems(){  _itemsUsed = 0;  _bytesUsed = 0;  _timeUsed = TimeSpan.FromSeconds(0);}

    然后通过其他属性和方法(如下所示)公开捕获的信息:

    public TimeSpan TimeUsed { get { return _timeUsed; } }public int ItemUsed { get { return _itemsUsed; } }public long ByteUsed { get { return _bytesUsed; } }public TimeSpan TimeCapacity { get { return TimeSpan.MaxValue; } } public int ItemCapacity { get { return int.MaxValue; } } public long ByteCapacity {   get { return (long)GetFreeSpace(_selectedDrive); } }

    Used 方法只是返回上述方法所维护的计数值。TimeCapacityItemCapacity 属性同时返回其类型各自的 MaxValue 值,因为计算实际用时和实际可用的项数远远超出了本文的讨论范围。ByteCapacity 使用我的私有 GetFreeSpace 方法(再次说明,它只是 Win32 GetDiskFreeSpaceEx 函数的一个 p/invoke 包装)来返回磁盘中的可用空间;当然,在与 ByteUsed 配合时这个值也没有什么用处,因为 ByteUsed 表示的是 DVR-MS 文件的大小,而 ByteCapacity 则用于确定磁盘中是否有空间来存放这些文件,但输出文件却是压缩过的 WMV 文件。不过这个实现细节您应该能够自如地进行更改。

    我还将介绍三个更加重要但实现很简单的属性:

    public MediaType SupportedMediaTypes {   get { return MediaType.RecordedTV; } } public bool OrderIsImportant { get { return true; } }public IBrandInfo BrandInfo { get { return this; } }

    SupportedMediaTypes 返回一个加标记的枚举,列出此外接程序支持的媒体类型:可能的类型包括图片、视频、音乐和录制的电视等,Media Center 通常支持所有这些媒体类型。然而,由于此外接程序的主要作用是将 DVR-MS 文件转换成 WMV 文件,因此我将其实现为只从 SupportedMediaTypes 返回 MediaType.RecordedTV

    Media Center 使用 OrderIsImportant 来确定是否应该允许用户对要处理的录制节目列表重排序。虽然顺序对此外接程序来说并不是真的很重要(因为它只是将文件写到硬盘中),但我想让用户安排某些特定节目在其他节目之前转换(图 12),所以我从这个属性返回 true 而不是 false。

    图 12. 对选定的节目重排序

    BrandInfo 属性允许外接程序的作者修改 Media Center 显示的 UI 以便包含特定于产品的信息。该属性返回一个实现 IBrandInfo 接口的对象。为简单起见,我只在我的外接程序中实现该接口并返回对该外接程序对象自身的引用:

    public class WmvTranscoderPlugin : MarshalByRefObject,   IAddInModule, ListMakerApp, IBrandInfo{  ...  public IBrandInfo BrandInfo { get { return this; } }  ...  public string ViewListPageTitle { get { return "Files to transcode"; } }  public string SaveListButtonTitle { get { return "Transcode"; } }  public string PageTitle { get { return "Transcode to WMV"; } }  public string CreatePageTitle { get { return "Specify target folder"; } }  public string ViewListButtonTitle { get { return "View List"; } }  public string ViewListIcon { get { return null; } }  public string MainIcon { get { return null; } }  public string StatusBarIcon { get { return null; } }  ...}

    IBrandInfo 的八个属性被分成两类:呈现在 UI 中的文本字符串和指定磁盘中图形位置的路径字符串。如果一个属性返回 null,则使用默认值。这样,由于我现在的图形艺术水平还有些欠缺,因此对所有图标属性我都返回 null。这些属性在 UI 中出现的位置如下表所示:


    --  作者:admin
    --  发布时间:2005-11-26 3:22:00
    -- 

    属性
    描述

    PageTitle

    当外接程序使用时显示在右上角的文本。

    CreatePageTitle

    列表创建页面的标题文本。

    SaveListButtonTitle

    用于在列表创建之后启动处理操作的按钮上的文本。

    ViewListButtonTitle

    用于查看要复制以进行处理的媒体项的按钮上的文本。

    ViewListPageTitle

    列表查看页面的标题文本。

    MainIcon

    包含要作为列表生成页面上主图标(水印)使用的图标的文件路径。

    StatusBarIcon

    包含 Media Center 放在生成页面左下角的图标的文件路径。

    ViewListIcon

    Media Center 放在列表查看页面顶部的图标文件的路径。


    --  作者:admin
    --  发布时间:2005-11-26 3:23:00
    -- 

    ListMakerApp 上最有趣的方法是 LaunchCancel 。一旦用户创建了要处理的文件列表并单击按钮开始处理,Media Center 就会调用 Launch 方法,它提供三个参数:用户选择的录制节目列表、可被调用以通知 Media Center 状态更新的进程更新委托和应该调用以通知 Media Center 处理完成(成功或因某种异常情况)的完成委托。Launch 方法的作用是立即返回并在后台线程中执行实际的工作。当用户选择取消处理时就会调用 Cancel 方法,然后由外接程序停止和终止其操作。

    WmvTranscoderPlugin 的实现遵循这种模式:将 Launch 的参数存储到成员变量中,然后将执行实际转换工作的 ConvertToWmv 方法排入 ThreadPool 队列中:

    public void Launch(ListMakerList lml, ProgressChangedEventHandler pce,   CompletionEventHandler ce){  _listMakerList = lml;  _progressChangedHandler = pce;  _completedHandler = ce;  _cancellationPending = false;  ThreadPool.QueueUserWorkItem(new WaitCallback(ConvertToWmv), null);}private void ConvertToWmv(object ignored){  ThreadPriority oldThreadPriority = Thread.CurrentThread.Priority;  Thread.CurrentThread.Priority = ThreadPriority.Lowest;  try  {    DirectoryInfo outDir = Directory.CreateDirectory(      _selectedDrive + ":" + _listMakerList.ListTitle);    _currentConvertingIndex = 0;    foreach(ListMakerItem item in _listMakerList.Items)    {      if (_cancellationPending) break;      string dvrMsName = item.Filename;      string wmvName = outDir.FullName + "" +         item.Name + ".wmv";      _currentConverter = new WmvConverter(        dvrMsName, wmvName, _profilePath);      _priorCompletedPercentage = _currentConvertingIndex /         (float)_listMakerList.Count;      _currentConverter.PollFrequency = 2000;      _currentConverter.ProgressChanged +=         new ProgressChangedEventHandler(ReportChange);      _currentConverter.Convert();      _currentConverter = null;      _currentConvertingIndex++;    }    _completedHandler(this, new CompletionEventArgs());  }   catch(Exception exc)  {    _completedHandler(this, new CompletionEventArgs(exc));  }  finally  {    Thread.CurrentThread.Priority = oldThreadPriority;  }}

    ConvertToWmv 在选定的驱动器上创建一个目录,使用用户指定的目标文件夹的名称(参见图 13)。然后该方法循环访问所提供的 ListMakerList 中的所有 ListMakerItem 对象,获取 DVR-MS 文件的路径并使用前面构建的 WmvConverter 来将目标目录中的每个 DVR-MS 文件转换成 WMV 文件。ConverterProgressChanged 事件关联到外接程序中的一个私有方法 — ReportChange 上,然后由该方法调用 Media Center 的进程更新委托。另外,当前转换程序存储在一个成员变量中,因而可以使用 Cancel 方法来停止其进程


    --  作者:admin
    --  发布时间:2005-11-26 3:23:00
    -- 

    Cancel 方法也非常简单。它设置了一个成员变量,用于警告在另一个线程中运行的 ConvertToWmv 方法,通知它用户已经请求取消。然而,正如您在 ConvertToWmv 方法中看到的,只有当该方法准备转换下一个 DVR-MS 文件时才会对此进行检查,所以 Cancel 方法还使用存储在一个成员变量中的 WmvConverter 对象,使用该 ConverterCancelAsync 方法取消当前执行的转换。正如我们前面所看到的,这将导致 Converter.RunGraph 方法从 WaitForCompletion 方法返回后即刻停止。

    public void Cancel(){  // Cancel any pending conversions  _cancellationPending = true;  // Cancel the current conversion  WmvConverter converter = _currentConverter;  if (converter != null) converter.CancelAsync();}

    我在本文的下载中包含了此外接程序的一个完整的工作实现,包括一个安装程序。该安装程序同时将 WmvTranscoderPlugin 的程序集和 WmvConverter 的程序集安装到全局程序集缓存 (GAC) 中,然后使用 RegisterMceApp.exe 工具来将此外接程序通知 Media Center。注册应用程序依赖于一个 XML 配置文件,如下所示:

            

    您应该能够运行安装程序并直接通过一个我们都不必编写的非常时髦的 UI 来将 DVR-MS 立即转换成 WMV。(感谢你,Media Center 团队!)

    图 14. 成功的编码转换

    返回页首

    访问 DVR-MS 元数据

    DVR-MS 文件格式既包含音频、视频和闭合字幕数据,也包含描述文件及其内容的元数据。一旦电视节目录制下来,节目的标题、描述、演员表和原始播放日期等信息就存储在这个位置。很酷的一点是,您的应用程序可以通过 DirectShow StreamBufferRecordingAttribute 对象实现的 IStreamBufferRecordingAttribute 接口轻松地访问此数据。这个对象可以使用它的 CLSID 来创建,正如我本文中创建其他 DirectShow 对象那样。

    要使用 IStreamBufferRecordingAttribute ,首先必须为它提供一个托管接口(您会在本文的代码下载中发现这段代码,它嵌套在 DvrmsMetadataEditor 类中):


    --  作者:admin
    --  发布时间:2005-11-26 3:24:00
    -- 

    [ComImport][Guid("16CA4E03-FE69-4705-BD41-5B7DFC0C95F3")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]private interface IStreamBufferRecordingAttribute{  void SetAttribute(    [In] uint ulReserved,     [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName,    [In] MetadataItemType StreamBufferAttributeType,    [In, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute,    [In] ushort cbAttributeLength);  ushort GetAttributeCount([In] uint ulReserved);  void GetAttributeByName(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName,    [In] ref uint pulReserved,    [Out] out MetadataItemType pStreamBufferAttributeType,    [Out, MarshalAs(UnmanagedType.LPArray)] byte[] pbAttribute,    [In, Out] ref ushort pcbLength);  void GetAttributeByIndex (    [In] ushort wIndex,    [In, Out] ref uint pulReserved,    [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszAttributeName,    [In, Out] ref ushort pcchNameLength,    [Out] out MetadataItemType pStreamBufferAttributeType,    [Out, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute,    [In, Out] ref ushort pcbLength);  [return: MarshalAs(UnmanagedType.Interface)]  object EnumAttributes();}

    为了访问 DVR-MS 文件的元数据,我构造了一个 StreamBufferRecordingAttribute 对象并获取它的 IFileSourceFilter 接口(您在本文前面也看到了相应的 IFileSinkFilter 接口;它们几乎完全相同)。IFileSourceFilterLoad 方法可用于打开我对其元数据感兴趣的 DVR-MS 文件,此时可以获取它的 IStreamBufferRecordingAttribute 接口并将该接口用于检索和编辑元数据:

    public class DvrmsMetadataEditor : MetadataEditor{  IStreamBufferRecordingAttribute _editor;  public DvrmsMetadataEditor(string filepath)  {    IFileSourceFilter sourceFilter = (IFileSourceFilter)      ClassId.CoCreateInstance(ClassId.RecordingAttributes);    sourceFilter.Load(filepath, null);    _editor = (IStreamBufferRecordingAttribute)sourceFilter;  }  ...}

    对元数据的读访问是通过 DvrmsMetadataEditor.GetAttributes 方法提供的,该方法提供了 IStreamBufferRecordingAttributeGetAttributeCountGetAttributeByIndex 方法的简单抽象。

    public override System.Collections.IDictionary GetAttributes(){  if (_editor == null)     throw new ObjectDisposedException(GetType().Name);  Hashtable propsRetrieved = new Hashtable();  ushort attributeCount = _editor.GetAttributeCount(0);  for(ushort i = 0; i < attributeCount; i++)  {    MetadataItemType attributeType;    StringBuilder attributeName = null;    byte[] attributeValue = null;    ushort attributeNameLength = 0;    ushort attributeValueLength = 0;    uint reserved = 0;    _editor.GetAttributeByIndex(i, ref reserved, attributeName,       ref attributeNameLength, out attributeType,       attributeValue, ref attributeValueLength);    attributeName = new StringBuilder(attributeNameLength);    attributeValue = new byte[attributeValueLength];    _editor.GetAttributeByIndex(i, ref reserved, attributeName,       ref attributeNameLength, out attributeType,       attributeValue, ref attributeValueLength);    if (attributeName != null && attributeName.Length > 0)    {      object val = ParseAttributeValue(        attributeType, attributeValue);      string key = attributeName.ToString().TrimEnd(/'//0/');      propsRetrieved[key] = new MetadataItem(        key, val, attributeType);    }  }  return propsRetrieved;}

    首先,使用 GetAttributeCount 方法来查明要检索的元数据项有多少。然后,对于每个属性,使用 GetAttributeByIndex 方法检索属性名的长度和值的长度(以字节为单位)(通过将 name 和 value 参数指定为空值)。当获得长度之后,我就可以创建大小适当的缓冲区来存储数据,并且可以再次调用 GetAttributeByIndex 来检索属性的真实名称和字节数组值。如果检索成功,则会根据属性的类型将存储该值的字节数组解析为适当的托管对象。我的 ParseAttributeValue 方法返回 GUID、无符号整型、无符号长整型、无符号短整型、字符串、布尔值或者原始数组(如果值是简单的二进制),这对大多数复杂的元数据属性都是通用的。然后使用该属性的名称及其类型和值构造一个新的 MetadataItem 实例,这个实例将添加到该文件的所有属性的 Hashtable 中。当所有属性都检索完毕时,此集合将返回给用户。

    SetAttributes 方法的工作方式则相反。它是随 MetadataItem 对象集合提供的,其中每个对象都根据其类型格式化为适当的字节数组,然后与 SetAttribute 方法一起使用,以便设置文件的元数据属性:

    public override void SetAttributes(IDictionary propsToSet){  if (_editor == null)     throw new ObjectDisposedException(GetType().Name);  if (propsToSet == null)     throw new ArgumentNullException("propsToSet");  byte [] attributeValueBytes;  foreach(DictionaryEntry entry in propsToSet)  {    MetadataItem item = (MetadataItem)entry.Value;    if (TranslateAttributeToByteArray(      item, out attributeValueBytes))    {      try      {        _editor.SetAttribute(0, item.Name,           item.Type, attributeValueBytes,           (ushort)attributeValueBytes.Length);      }      catch(ArgumentException){}      catch(COMException){}    }  }}

    MetadataItem 是一个属性的名称、值和类型的简单包装。MetadataItemType 是有效类型(GUID、字符串、无符号整型等)的枚举。


    --  作者:admin
    --  发布时间:2005-11-26 3:24:00
    -- 

    您可能注意到 DvrmsMetadataEditor 类是从 MetadataEditor 基类派生的。我这样做是为了提供另一个类 — AsfMetadataEditor ,它也是从 MetadataEditor 派生的。AsfMetadataEditor 基于包含在 Windows Media Format SDK(从此处下载 SDK )中的示例代码。它使用 Windows Media IWMMetadataEditorIWMHeaderInfo3 接口来获取 WMA 和 WMV 文件(这两者都基于 ASF 文件格式)的相关元数据信息。您可能会发现,当前这些 Windows Media Format SDK 接口除了能用于处理 WMA 和 WMV 文件外,还可以处理 DVR-MS 文件,不过将来可能不再这样,而且 Microsoft 强烈建议使用 IStreamBufferRecordingAttribute 接口来处理 DVR-MS 文件。IWMHeaderInfo3 接口的相关部分与 IStreamBufferRecordingAttribute 接口几乎相同,因此 AsfMetadataEditor 类和 DvrmsMetadataEditor 类也极其相似。

    在这些类就位后,将元数据从一个媒体文件复制到另一个(例如从 DVR-MS 文件复制到经过代码转换的 WMV 文件)就变得极为简单,从而让您保持与经过编码转换的 TV 录制相关联的元数据的保真度:

    using(MetadataEditor sourceEditor = new DvrmsMetadataEditor(srcPath)){  using(MetadataEditor destEditor = new AsfMetadataEditor(dstPath))  {    destEditor.SetAttributes(sourceEditor.GetAttributes());  }}

    实际上,正是出于从一个媒体文件向另一个媒体文件复制元数据的目的,我在 MetadataEditor 类中创建了一个静态的 MigrateMetdata 方法,这个方法不仅能按上述方式迁移元数据,而且对它加以扩大,这样在 Media Player 中查看 DVR-MS 文件和在 Media Center 中播放 WMV 文件时,就可以显示更多的可用信息。

    返回页首

    编辑 DVR-MS 文件

    除了转换为 WMV 之外,编辑和拼接 DVR-MS 文件可能是我在网上新闻组中看到的第二个最常请求的功能。许多人没有意识到的是,DirectShow RecComp 对象及其 IStreamBufferRecComp 接口提供了现成的拼接功能。IStreamBufferRecComp 接口用于从现有的录制片段创建新的录制,以及将来自一个或多个 DVR-MS 文件的片段连接在一起。

    IStreamBufferRecComp 接口非常简单,它的一个 C# 导入如下所示:

    [ComImport][Guid("9E259A9B-8815-42ae-B09F-221970B154FD")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IStreamBufferRecComp{  void Initialize(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszTargetFilename,     [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecProfileRef);  void Append(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording);  void AppendEx(    [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording,    [In] ulong rtStart, [In] ulong rtStop);  uint GetCurrentLength();  void Close();  void Cancel();}

    要拼接 DVR-MS 文件,首先要创建 RecComp 对象的实例。这可以通过本文前面介绍的 ClassId.CoCreateInstance 方法来完成,代码如下:

    IStreamBufferRecComp recCom =   (IStreamBufferRecComp)ClassId.CoCreateInstance(ClassId.RecComp)and with ClassId.RecComp defined aspublic static readonly Guid RecComp =   new Guid("D682C4BA-A90A-42FE-B9E1-03109849C423");

    有了 IStreamBufferRecComp 之后,就可以使用它的 Initialize 方法来为新的录制指定输出文件名。另外,Initialize 的第二个参数应该是要拼接的其中一个 DVR-MS 输入文件的文件路径。IStreamBufferRecComp 支持连接来自一个或多个文件的片段,但所有这些文件必须使用相同的配置文件录制,这意味着它们必须使用 Media Center 中的相同配置和设置进行录制。RecComp 需要知道输出文件使用什么配置文件,因此您必须指定一个输入文件作为第二个参数,以便它可以检查其配置文件信息并将该信息作为输出文件的基础。

    一旦初始化了 IStreamBufferRecComp ,您就可以开始构建新文件。调用 Append 方法,指定一个 DVR-MS 输入文件的完整路径,则整个文件就会追加到输出文件中。AppendEx 方法允许您指定附加的开始和停止时间,以便只使用输入文件的一部分并将这部分追加到输出文件中。在非托管接口中,这些时间被定义为 REFERENCE_TIME — 一个代表以 100 毫微秒为单位的数值的 64 位长整数值,所以在托管代码中,您可以使用如下所示的函数来将秒转换成传递给 AppendExREFERENCE_TIME 值:

    internal static ulong SecondsToHundredNanoseconds(double seconds){  return (ulong)(seconds * 10000000);}

    当您完成追加到输出文件时,Close 方法就会关闭输出文件。在您连接到该文件时,可以使用一个单独线程的 GetCurrentLength 方法来确定输出文件的当前长度。然后您可以使用此信息和您对输入文件/片段长度的了解来计算完成拼接的百分比。请注意,这个过程非常快,因为将片段从一个 DVR-MS 文件追加到另一个文件并不需要编码和解码。


    --  作者:admin
    --  发布时间:2005-11-26 3:25:00
    -- 

    为了演示此接口,我构建了 DVR-MS 编辑器应用程序(如图 15 所示),并将它作为与本文有关的代码下载的一部分。

    图 15. DVR-MS 编辑器

    这个应用程序其实非常简单,用了一个多小时就实现了。它使用 Windows Media Player ActiveX 控件来显示输入的视频文件。为了加载视频文件,它将 AxWindowsMediaPlayer.URL 属性设置为 DVR-MS 文件的路径,这样可以使 Media Player 加载该视频(如果 AxWindowsMediaPlayer.settings.autoStart 属性为真,它还会开始播放)。

    一旦加载了视频,用户就可以使用“Media Player”工具栏对它进行控制,这个工具栏可以使用户完全控制视频的播放和搜索。当到达用户想要开始或停止一段视频的位置时,就会查询 AxWindowsMediaPlayer.Ctlcontrols.currentPosition 属性。然后,刚才描述的 IStreamBufferRecComp 接口可以使用这些时间来创建输出文件。

    另外,Media Player 对视频的当前位置提供了细粒度的编程控制。您可以使用如下所示的代码来逐帧移动视频:

    ((WMPLib.IWMPControls2)player.Ctlcontrols).step(1);

    或者,还可以通过设置刚才讨论的 AxWindowsMediaPlayer.Ctlcontrols.currentPosition 来跳转到视频中的特定位置。

    DVR-MS 编辑器应用程序还利用了本文前面描述的一些其他技术,例如将元数据从源视频文件复制到输出视频文件。

    返回页首

    小结

    这是令人惊讶的技术,不是吗?DirectShow 和 Windows XP Media Center Edition 团队为开发人员提供了许多处理 DVR-MS 文件的工具(包括非托管代码的和托管代码的)。通过使用这些工具,可以创建新的应用程序来提供大多数人没有意识到他们能够使用的真正强大的功能。本文所讨论的主题只涉及到您可以用来处理 DVR-MS 文件的各种技术的一部分,而在人们编写的使用这些库和工具的解决方案中,它们所占的比例则甚至更小。我期待着获悉您使用这种功能来开发解决方案。

    现在,我要回去看会电视了。

    返回页首

    相关书籍

    Programming Microsoft DirectShow for Digital Video and Television (Microsoft Press, 2003)

    Fundamentals of Audio and Video Programming for Games (Microsoft Press, 2003)

    返回页首

    致谢

    我衷心感谢 Matthijs Gates、Aaron DeYonker、Ryan D/'Aurelio、Ethan Zoller、Eric Gunnerson 和 Alex Seigler 提供他们的研究领域的专家见解,感谢 ABC 允许我使用来自他们电视节目的示例和屏幕快照,也要感谢我的好朋友 John Keefe 和 Eden Riegel,感谢他们允许我在本文中使用他们的肖像。

    关于作者

    Stephen Toub 是 MSDN Magazine 的技术编辑,他还为该杂志撰写 .NET Matters 专栏。


    < type="text/javascript"> //鼠标双击自动滚屏 var currentpos,timer; function initialize() {timer=setInterval("scrollwindow()",10);} function sc(){clearInterval(timer); } function scrollwindow() {currentpos=document.body.scrollTop; window.scroll(0,++currentpos); if (currentpos != document.body.scrollTop) sc();} document.οnmοusedοwn=sc document.οndblclick=initialize function New(para_URL) {var URL =new String(para_URL) window.open(URL,'','resizable,scrollbars')}

    Copyright ©2002 - 2005 learnsky
    执行时间:3,921.87500 毫秒。查询数据库3次。

    展开全文
  • DirectShow

    热门讨论 2012-03-29 20:29:05
    抽取的DirectShow工具 内含include文件,lib文件和dll文件
  • Directshow的优点与缺点

    千次阅读 2018-02-28 13:48:54
    Directshow是微软研发出来的一个多媒体框架,它能实现常见多媒体应用的功能,比如文件/流的接收读取 、AV文件的解交织(Demux)、音视频的解码、音视频的同步、音视频的展现等。曾几何时,Directshow是Windows平台的...
  • directshow9

    2018-06-20 16:05:34
    directshow9 开发头文件和库
  • DirectShow 简介

    2018-03-22 16:55:59
    Microsoft DirectShow是用于MicrosoftWindows®平台上的流媒体的体系结构。 DirectShow提供高质量的多媒体数据流采集和回放。 它支持各种格式,包括Advanced Systems Format(ASF),Motion Picture Experts Group...
  • 一个清华学子写的关于directshow的学习心得   学习DirectShow有一段时间了,把这段学习过程中翻译出来的SDK与大家分享,同时也希望专家们指出我理解上的错误,万分感谢。1. DirectShow介绍  DirectShow是一个...
  • FFmpeg获取DirectShow设备数据(摄像头,录屏)

    万次阅读 多人点赞 2014-08-02 00:57:27
    这两天研究了FFmpeg获取DirectShow设备数据的方法,在此简单记录一下以作备忘。1. 列设备 ffmpeg -list_devices true -f dshow -i dummy命令执行后输出的结果如下(注:中文的设备会出现乱码的情况):我自己的...
  • [总结]FFMPEG视音频编解码零基础学习方法

    万次阅读 多人点赞 2013-11-16 00:04:05
    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者。在和大家探讨的过程中,我忽然发现了一个问题:在“大神”和...
  • 分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇...分享知识,造福人民,实现我们中华民族伟大复兴!&nbsp;&nbsp;&nbsp;... 这两天研究了FFmpe
  • ffmpeg,h.264和opencv关系及视频编码与封装格式 一.ffmpeg,h.264和opencv的关系 二.视频编码与封装格式 三.播放视频
  • Directshow使用ffmpeg构建解码filter

    千次阅读 2016-01-03 09:29:55
    http://blog.csdn.net/zhengxinwcking/article/details/30475869
  • Linux和window下几种流行的音频视频编程框架作一个总结,防止自己迷惘,免于晕头转向。 一、GStreamer GStreamer is a library that allows the construction of graphs of media-handlingcomponents, ranging...
  • 第一个版本 ### 软件版本及实现功能 0.0.1  1. USB摄像头枚举和设备...软件基于 DirectShowFFMpeg开发 ### 软件使用说明: 1. 第一次使用(以管理员权限运行)打开系统配置进行设备获取
  • FFMPEG列出DirectShow支持的设备

    千次阅读 2017-01-16 14:17:11
    FFMPEG列出dshow支持的设备: [plain] view plain copy   ffmpeg -list_devices true -f dshow -idummy  举例: 采集摄像头和麦克风 [plain] view plain copy   ffmpeg
  • 使用FFmpeg转录网络直播流

    万次阅读 多人点赞 2016-10-04 22:12:12
    爱奇艺万能播放器的最新版本增加了一个播放网络流的功能。不过,入口藏在播放器区域的右键菜单里,不太好找: ... 有时候,看到精彩的直播内容,想把某些片段...那么,开个小窗给FFmpeg吧,它可以搞定! 命令行如...
  • 基于FFmpeg的RTSP directshow filter

    千次阅读 2014-09-01 17:26:45
    基于FFmpeg的RTSP directshow filter,注册好filter,可以直接render rtsp url
  • 24小时不间断录像,录像文件支持暴风影音播放### 软件说明:软件基于 DirectShowFFMpeg开发### 软件使用说明:1. 第一次使用(以管理员权限运行)打开系统配置进行设备获取2. 主界面双击树节点会打开摄像头视频 3....
  • linux和window下几种流行的音频视频编程框架作一个总结,防止自己迷惘,免于晕头转向。 一、GStreamerGStreamer is a library that allows the construction of graphs ofmedia-handling components, ranging from ...
  • 在视频聊天、视频会议、在线监控和视频展台等项目中,需要查找出本地电脑上连接的所有摄像头,网上流传比较多的方式是ffmpeg的方式,这种方式可以跨平台,不同的平台下调用不同的库。这种方式在控制台直接打印了...
  • VC++实现视频聊天:VFW视频采集+FFmpeg编码

    千次阅读 热门讨论 2017-11-27 20:02:02
    上次介绍了视频聊天软件的界面、文字聊天、文件传输部分,这此介绍视频聊天功能,这算是音视频领域一个很广的应用。首先视频聊天的双方需要有一个USB摄像头(或者笔记本摄像头),在windows系统下,一个完整的视频...
  • 最近尝试用directshow写filter,但要用到ffmpeg库的实现,遇到几个奇怪的问题,够折磨的,现在把问题的原因说一下。 由于directshow的filter是c++的dll工程,所以必须在包含ffmpeg的头文件前加上: extern "C" { ...
  • AsyncReadFilter 将由SDK自带的例子async修改而来,async是把文件先全部读取到内存然后播放。现在要把它修改成从文件直接读取数据。
  • 对于linux和mac党,请自行搜索关键字"ffmpeg X11",以上. 一句话介绍:注册录屏dshow滤镜(例如screen-capture-recorder或者uscreen capture),然后通过dshow获取录屏图像来压制,例如ffmpeg可以从dshow的对应滤镜获取...
  • 本文转自http://hi.baidu.com/gaomanyi/blog/item/f5d172598107572c2934f048.html音频视频编程相关:GStreamer/ffmpeg/directshow/vfwlinux和window下几种流行的音频视频编程框架作一个总结,防止自己迷惘,免于...
  • DirectShow捕获+mencoder+ffmpeg+sox 打造小巧的音视频制作、加工软件 DirectShow捕获+mencoder+ffmpeg+sox  打造小巧的音视频制作、加工软件  捕获音视频并进行加工处理,是常见的应用。录制一段小...
  • ffmpeg是個優秀的開放原始碼專案,其中提供了許多常見、不常見的video/audio的codecs。你也可以輕易的找到它的Win32 build。透過ffmpeg,我們可以很容易的解決...不過,ffmpeg並沒有提供與DirectShow整合的方式及介面

空空如也

1 2 3 4 5 ... 20
收藏数 106,836
精华内容 42,734
关键字:

directshow