DES(DirectShow Editing Services)是一套基于DiretShow核心技术框架的编程接口。它的出现简化了视频编缉任务,弥补了DirectShow对媒体文件非线性编辑支持的先天性不足。
DES的构架
Timeline:组织各个媒体源、音视频效果、过渡效果等的信息集合,实际上代表了最终的视频剪辑作品。
XML Parser:将Timeline结构转化为XML格式文件进行保存,或者从XML文件生成对应的Timeline。
Render Engine:将Timeline实际转化为DShow Filter实现输出控制引擎。
Media Locator:用于定位媒体文件。
基于Timeline的DES模型
在DES中,Timeline被用来表示一个视频剪辑工程(Video Editing Project),暂且称之为视频剪辑过程。剪辑的过程,如果没有媒体源是不行的。而在DES中,媒体源可以是音视频文件,甚至可以是图片。一个个媒体源(Source)线性的连接就构成了一个Track。需要注意的是,我们在使用DES建立Timeline的时候,音频和视频要处在不同的Track之上。
多个音频和视频Tracks的处理,我们可以向其中加入Effects(效果),以及视频之间的Transitions(过渡效果)。DES提供了100多种SMPTE过渡效果(Transitions),同时我们还可以使用IE自带的各种音视频效果(Effect),或者Transitions(过渡)。(如果想知道具体有什么过渡效果,可以运行DXSDK ROOT\Samples\C++\DirectShow\Bin\TransViewer.exe查看。
/////
技术原理
DES (DirectShow Editing Services),是一套基于DirectShow核心框架的编程接口。DES的出现,简化了视频编辑任务,弥补了DirectShow对于媒体文件非线性编辑支持的先天性不足。但是,就技术本身而言,DES并没有超越DirectShow Filter架构,而只是DirectShow Filter的一种增强应用。我们可以从下图中了解到DES在我们整个多媒体处理应用中的位置。
下面,我们举个例子来看一下DES能够给我们带来些什么。假如我们现在有三个文件A、B和C,使用这三个文件做成一个合成的文件。我们想取A的4秒钟的内容,紧接着取B的10秒钟的内容,再紧接着C的5秒钟的内容。如果仅仅是这样,我们直接使用DirectShow Filter是不难实现的。(一般情况下,应用程序级会维持各个文件的编辑信息,由应用程序根据这些信息动态创建/控制功能单一的Filter Graph,以顺序对各个文件进行处理。)但是,如果我们的"创意"是随时改变的,我们现在想让C在B之前出现,或者我们想取A的不同位置的10秒钟内容,或者我们想给整个合成的文件加上一段美妙的背景音乐。如果我们仍然直接使用DirectShow Filter去实现,情况就变得很复杂了。然而,对于DES,这真的是小Case!(将所有的编辑信息以DES提供的接口告诉DES,其它的如Filter Graph的创建/控制输出,就完全交给DES来负责吧!这时候,DES创建的Filter Graph带有各个Source输出的控制功能,一般比较复杂。)
如果我们使用DES,我们还可以得到如下的便利:
1. 基于时间线(Timeline)的结构以及Track的概念,使得多媒体文件的组织、编辑变得直观而高效;
2. 支持即时的预览;
3. 视频编辑项目支持XML文档的形式保存;
4. 支持对视频/音频的效果处理,以及视频之间切换的过渡处理;
5. 可以直接使用DES提供的100多种SMPTE过渡效果,以及MS IE自带的各种Transform、Transition组件;
6. 支持通过色调、亮度、RGB值或者alpha值进行图像的合成;
7. 自动对源文件输出的视频帧率、音频的采样率进行调整,直接支持视频的缩放。
接下去,我们来看一下DES的结构(Timeline模型),如下图所示:
这是一个树形结构。在这棵树中,音视频文件是叶结点,称作为Source;一个或多个Source组成一个Track,每个Track都有统一的媒体格式输出;Track的集合称作为Composition,每个Composition可以对其所有的Composition或Track进行各种复杂的编辑;顶级的Composition或Track就组成了Group;每个Group输出单一格式的媒体流,所有的Group组成一个Timeline, Timeline表示一个视频编辑的项目,它是这棵树的根节点。一个Timeline项目必须至少包含一个Group,最典型的情况一般包含两个Group:Audio Group和Video Group。
下面,我们来看一个典型的基于Timeline的Source Track编排。如下图:
图中,箭头方向即是Timeline的方向。这个Timeline由两个Group组成,每个Group中包含两个Source Track。在Group中,Track是有优先级的(Track 0具有最低的优先级,依次类推)。运行时,总是输出高优先级的Track中的Source内容。如果此时高优先级的Track中没有Source输出,则让低优先级的Track中的Source输出。如上图中Video Group的输出顺序为Source A->Source C->Source B。而对于Audio Group,它的所有Track的输出只是简单的合成。
我们再看一个典型的Track之间加入了Transition的Timeline结构。如下图:
图中,Video Group中是两个Track以及Track上几个Source的编排;Rendered video中表示这个Group最终输出的效果。我们可以看到,在Track 1上有一个Transition,表示这个时间段上从Track 0过渡到Track1的效果。一般,Transition位于高优先级的Track上。Transition也是有方向的,默认是从低优先级的Track过渡到高优先级的Track。当然,我们也可以改变Transition的方向。如下图所示,第一个Transition是从Track 0到Track 1,第二个Transition是从Track 1到Track 0。
值得注意的是,DES使用的Transition采用了叫做DirectX Transform Object的技术。任何两输入一输出的DirectX Transform Object都可以用作Transition。遗憾的是,微软现在的DirectX SDK不再支持这种组件的开发。我们能够使用的,只有DES本身提供的几种效果,还有就是Microsoft Internet Explorer自带的效果。DES使用的Effect情况类似,只不过DES Effect是单输入单输出的DirectX Transform Object。
//////
从上面的架构图中可以看出,多个Track(可以是一个)构成Composition,而多个Composition(同样可以是一个)构成Group,而Group(s)将构成Timeline。如果剪辑过程包括音视频,那么该Timeline至少有2个Group(Video Group & Audio Group)。
上面是SDK里面一个示例Timeline的时间抽象图。对于在时间上有重叠的情况,DES将采取以下方法:
1. 对于Video Group,Track1的优先级高于Track0,如果发上重叠现象,Track1上面的图像将被显示。
2. 对于Audio Group, DES将采用混合(Mixing)的方式进行处理。
在建立Timeline的时候,我们涉及两个时间概念:
1. 时间线时间(Timeline Time):相对于整个时间线项目的时间。
2. 媒体时间(Media Time):相对于媒体源的时间,就是指相对于媒体文件开头的时间。
使用Timeline
创建Timeline
我们就拿SDK的例子来讲解如何创建Timeline。该例子位于DXSDK ROOT\Samples\C++\DirectShow\Editing\TimelineTest。
首先我们会注意到在TimelineTest.cpp的开头一个预处理宏:
#001 #ifdef STRICT
#002 #undef STRICT
#003 #endif
让我们来了解一下STRICT宏。在MSDN里面,STRICT宏作用是用来进行严格的类型检测。Windows.h头文件定义了一系列的宏,结构等用来使编译出来的代码可以在不同的Windows版本之间运行,当我们在定义了STRICT的环境下编译时,四种数据的类型将改变(下面只列举了一种情况,其他的可以参见MSDN):
1. 当我们在没有定义STRICT的时候,Windows的所有的HANDLE都被视为interger,意思就是说如果我们将一个HWND传递给HDC的时候,这种情况将是被允许的。反之,如果定了STRICT,编译器将报错。因为此时编译器进行了严格的类型检测。
让我们重新回到该示例程序。该示例程序Timeline的建立过程就在函数TimelineTest里面。
变量的申明部分:
#001 CComPtr< IRenderEngine > pRenderEngine;
#002 CComPtr< IGraphBuilder > pGraph;
#003 CComPtr< IVideoWindow > pVidWindow;
#004 CComPtr< IMediaEvent > pEvent;
#005 CComPtr< IAMTimeline > pTimeline;
#006 CComPtr< IAMTimelineObj > pVideoGroupObj;
#007 CComPtr< IAMTimelineObj > pAudioGroupObj;
#008 CComPtr< IMediaControl > pControl;
#009 CComPtr< IMediaSeeking > pSeeking;
所有的变量什么都采用了ATL Libraries里面的CComPtr(该宏的作用有点像智能指针,在使用的时候不用担心资源的释放问题)。第一个变量的申明IRenderEngine接口,该接口的作用是通过建立好的Timeline来建立Filter Graph供以后的预览或者输出文件。
那我们应该如何创建Timeline呢?首先我们来回忆一下COM变量的创建过程。创建一个COM实体,我们可以通过CoCreateInstance和QueryInterface。那么两个有什么不同呢。CoCreateInstance通过特定的CLSID创建一个没有初始化的实体(对象)。而QueryInterface的调用是为了获取调用对象的接口以提供另外的操作。
所以我们首先应该创建一个Timeline对象(所有的异常处理都被省略):
#001 hr = CoCreateInstance(
#002 CLSID_AMTimeline,
#003 NULL,
#004 CLSCTX_INPROC_SERVER,
#005 IID_IAMTimeline,
#006 (void**) &pTimeline
#007 );
接下来我们将创建Video Group。
#001 hr = pTimeline->CreateEmptyNode( &pVideoGroupObj, TIMELINE_MAJOR_TYPE_GROUP );
#002
#003 CComQIPtr< IAMTimelineGroup, &IID_IAMTimelineGroup > pVideoGroup( pVideoGroupObj );
#004
#005 CMediaType VideoGroupType;
#006
#007 // all we set is the major type. The group will automatically
#008 // use other defaults
#009 VideoGroupType.SetType( &MEDIATYPE_Video );
#010 hr = pVideoGroup->SetMediaType( &VideoGroupType );
我们通过IAMTimeline::CreateEmptyNode创建IAMTimelineObj接口,此时我们获得了该接口的一个指针。IAMTimeline::CreateEmptyNode的第二个参数传递的要创建object的枚举类型,该枚举类型的定义如下:
#001 typedef enum {
#002 TIMELINE_MAJOR_TYPE_COMPOSITE = 1,
#003 TIMELINE_MAJOR_TYPE_TRACK = 2,
#004 TIMELINE_MAJOR_TYPE_SOURCE = 4,
#005 TIMELINE_MAJOR_TYPE_TRANSITION = 8,
#006 TIMELINE_MAJOR_TYPE_EFFECT = 16,
#007 TIMELINE_MAJOR_TYPE_GROUP = 128
#008 } TIMELINE_MAJOR_TYPE;
值得注意对是,每个DES对象都是实现了IAMTimelineObj接口,而且各个具体的对象实现了各自特殊的接口,参考如下:
1. Source: IAMTimelineSrc, IAMTimelineEffectable, IAMTimelineSplittable;
2. Track: IAMTimelineTrack, IAMTimelineVirtualTrack,, IAMTimelineEffectable, IAMTimelineTransable, AMTimelineSplittable;
3. Composition: IAMTimelineComp, IAMTimelineVirtualTrack, IAMTimelineEffectable, IAMTimelineTransable;
4. Group: IAMTimelineGroup, IAMTimelineComp;
5. Effects: IAMTimelineEffect, IAMTimelineSplittable;
6. Transitions: IAMTimelineTrans, IAMTimelineSplittable;
创建了还不行,我们必须将Group加入到Timeline中:
#001 hr = pTimeline->AddGroup( pVideoGroupObj );
同样的过程我们创建一个Track,并且把它加入到Timeline中:
#001 CComPtr< IAMTimelineObj > pTrack1Obj;
#002 hr = pTimeline->CreateEmptyNode( &pTrack1Obj, TIMELINE_MAJOR_TYPE_TRACK );
#003
#004 //--------------------------------------------
#005 // tell the composition about the track
#006 //--------------------------------------------
#007
#008 CComQIPtr< IAMTimelineComp, &IID_IAMTimelineComp > pRootComp( pVideoGroupObj );
#009 hr = pRootComp->VTrackInsBefore( pTrack1Obj, -1 );
加入的过程我们调用的VTrackInsBefore,该函数的第二个参数是插入的track在composition中优先级。如果要将该Track插入到优先级的最后(意思就是默认的插入),使用参数-1。
加入Source,并且设置Source在Timeline的时间和媒体源的时间(Media Time):
#001 CComPtr<IAMTimelineObj> pSource1Obj;
#002 hr = pTimeline->CreateEmptyNode( &pSource1Obj, TIMELINE_MAJOR_TYPE_SOURCE );
#003
#004 // set up source right
#005 //
#006 hr = pSource1Obj->SetStartStop( TLStart, TLStop );
#007 CComQIPtr< IAMTimelineSrc, &IID_IAMTimelineSrc > pSource1Src( pSource1Obj );
#008
#009 hr |= pSource1Src->SetMediaTimes( MediaStart, MediaStop );
#010 hr |= pSource1Src->SetMediaName( pClipname );
上面的过程已经建立好Timeline,并且建立好了Source、Track和Composition。如果没有另外的Effects和Transitions的加入,我们就可以建立Filter Graph然后预览。不过我们还是来介绍如何加入Transition吧。
建立Transitions
建立Transitions的过程(其实包括所有的IAMTimelineObj的创建过程)实际上是调用IAMTimeline的接口CreateEmtpyNode(他们传递的类型参数不同而已)。
#001 CComPtr<IAMTimelineObj> pTrackTransObj;
#002 hr = pTimeline->CreateEmptyNode(&pTrackTransObj,
#003 TIMELINE_MAJOR_TYPE_TRANSITION );
然后我们设置采用的Transitions(这里我们用DXT_Jpeg)和过渡的执行时间。
#001 REFERENCE_TIME TransStart = 0 * UNITS;
#002 REFERENCE_TIME TransStop = 4 * UNITS;
#003
#004 // we set the CLSID of the DXT to use instead of a
#005 // pointer to the actual object. We let the DXT
#006 // have it's default properties.
#007 //
#008 hr = pTrackTransObj->SetSubObjectGUID( CLSID_DxtJpeg );
#009 hr |= pTrackTransObj->SetStartStop( TransStart, TransStop );
#010
#011 CComQIPtr< IAMTimelineTrans, &IID_IAMTimelineTrans > pTrackTrans( pTrackTransObj );
#012 hr |= pTransable->TransAdd( pTrackTransObj );
不过现在我们还不能就此结束,对于DXT_Jpeg的过渡,其过渡方式有207个,如何决定采用那个呢,MSDN上面有详细的介绍。当我们选择好了过渡方式之后,又该如何来设置它呢,不用慌,下面给出了示例代码。
#001 CComPtr< IPropertySetter > pTransSetter;
#002 hr = CoCreateInstance( CLSID_PropertySetter, NULL, CLSCTX_INPROC_SERVER,
#003 IID_IPropertySetter, (void**) &pTransSetter );
#004
#005 DEXTER_PARAM Param;
#006 CComBSTR ParamName( "MaskNum" ); // the property name
#007 Param.Name = ParamName;
#008 Param.nValues = 1; // how many values we want to set
#009
#010 DEXTER_VALUE Value;
#011 memset( &Value, 0, sizeof( Value ) );
#012 VariantClear( &Value.v );
#013 V_I4( &Value.v ) = 128; // mask number 128
#014 V_VT( &Value.v ) = VT_I4; // integer
#015
#016 hr = pTransSetter->AddProp( Param, &Value );
#017 hr |= pTrackTransObj->SetPropertySetter( pTransSetter );
音频的处理跟视频差不多,这里就不介绍了。如果需要,打开SDK里面该源代码,他会向你讲解的。现在就差不多了J,接下来我们进行预览。
预览Timeline
整个的过程我都省去了异常处理,不过这儿我还是得介绍一下ValidateSourceNames,该函数检查Source的有效性,当我们在预览或者输出文件之间,进行文件源的有效性是有必要的(我曾经陷入过死循环中)L。
#001 //----------------------------------------------
#002 // make sure files are in their correct location
#003 //----------------------------------------------
#004
#005 hr = pTimeline->ValidateSourceNames(
#006 SFN_VALIDATEF_CHECK | SFN_VALIDATEF_POPUP | SFN_VALIDATEF_REPLACE,
#007 NULL,
#008 0 );
#009 ASSERT( !FAILED( hr ) );
说了这么久,IRenderEngine也该出场了吧。让我们来建立IRenderEngine:
#001 hr = CoCreateInstance(
#002 CLSID_RenderEngine,
#003 NULL,
#004 CLSCTX_INPROC_SERVER,
#005 IID_IRenderEngine,
#006 (void**) &pRenderEngine );
回想一下IRenderEngine的作用,该接口的作用是通过建立好的Timeline来建立Filter Graph供以后的预览或者输出文件。所以我们要把Timeline的信息传递给它。
#001 // tell the render engine about the timeline it should look at
#002 //
#003 hr = pRenderEngine->SetTimelineObject( pTimeline );
接下来的过程很简单了。运用ConnectFrontEnd连接Timeline部分所建立的Filter,RenderOutputPins来预览(自动建立Renderer和连接Filter进行预览)。
#001 //--------------------------------------------
#002 // connect up the front end, then the back end
#003 //--------------------------------------------
#004
#005 hr = pRenderEngine->ConnectFrontEnd( );
#006 hr |= pRenderEngine->RenderOutputPins( );
最后的过程就是运行Graph了。
#001 hr = pRenderEngine->GetFilterGraph( &pGraph );
#002 hr |= pGraph->QueryInterface( IID_IMediaEvent,
#003 (void**) &pEvent );
#004 hr |= pGraph->QueryInterface( IID_IMediaControl,
#005 (void**) &pControl );
#006 hr |= pGraph->QueryInterface( IID_IMediaSeeking,
#007 (void**) &pSeeking );
#008 hr |= pGraph->QueryInterface( IID_IVideoWindow,
#009 (void**) &pVidWindow );
#010
#011 long lStyle=0;
#012 hr = pVidWindow->get_WindowStyle(&lStyle);
#013
#014 lStyle &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU);
#015 hr = pVidWindow->put_WindowStyle(lStyle);
#016
#017 //--------------------------------------------
#018 // run it
#019 //--------------------------------------------
#020
#021 hr = pControl->Run( );
到此结束。写文件部分请看DES如何写文件。