About YUV Video

2010-3-10 21:49 | by 2ndboy

  今天在看 Media Foundation 文档的时候翻到一篇叫“About YUV Video”文章,感觉对理解 YUV color space 很有帮助,一激动就翻译了;),晾在这里分享一下。

注 1:为了严格区分 luma、luminance 和 brightness,故保留 luma 和 luminance 未译。

注 2:在表示单独的色彩饱和度时,将 chroma 译作饱和度;在表示 U、V 这两个量时,将 chroma 译作色度。

关于 YUV 视频

数字视频经常被编码成 YUV 格式。这篇文章解释了 YUV 视频的一般概念和一些术语,而不会深入到 YUV 视频处理的数学理论中去。

如果你曾经接触过 CG,你大概比较熟悉 RGB 颜色。一个 RGB 色彩由三个值构成:红、绿和蓝。这些值直接对应于可见光谱的一部分。这三个 RGB 值构成的数学坐标系统称作颜色空间(color space)。红色分量定义了这个坐标系统中的一个轴,蓝色分量定义了第二个,绿色定义了第三个。任何一个 RGB 颜色都会落在这个颜色空间的某处。例如纯品红是 100% 的蓝色,100% 的红色和 0% 的绿色。

尽管 RGB 是一种常见的颜色表示方法,但是用其它坐标系统来表示颜色也是可行的。术语 YUV 代表了一族颜色空间,它们的共同点是把亮度信息和色彩信息分开来编码。跟 RGB 一样,YUV 也用三个值来表示所有颜色。这些值被称为 Y’、U 和 V(事实上,“YUV”这种用法严格来讲是不正确的。在计算机视频里,YUV 几乎总是代表一个我们稍后要进行讨论的叫 Y’CbCr 的颜色空间。尽管如此 YUV 经常被当作一个通用术语来表示那些跟 Y’CbCr 有着相同原来的颜色空间)。

Y’(也被称作 luma)用来表示颜色的亮度值。撇号(’)用来把 luma 跟另外一个意义接近的值加以区分区分,这个值就是 luminance(这个值用 Y 做标记)。Luminance 源自线性 RGB 值,而 luma 源自非线性(gamma-corrected)RGB 值。Luminance 是一个跟真实亮度接近的值,但是由于技术原因,实践中 luma 更常用一些。撇号经常被省略,但是 YUV 颜色空间总是使用 luma 而不是 luminance。

Luma 源自 RGB 颜色,但是要在红、绿、蓝分量上做个加权平均。对于标清(standard-definition)视频, 使用如下公式:

Y’ = 0.299R + 0.587G + 0.114B

这个公式反应了人眼对某些特定波长的光更加敏感这一事实,这关系到我们对颜色中亮度的感知。蓝光看上去最暗淡,绿光看上去最亮,红光则介乎其间。这个公式也反映了被用在早期电视中的磷的特性。一个重新考虑了现代电视技术的新公式被用在高清(high-definition)电视上:

Y’ = 0.2125R + 0.7154G + 0.0721B

跟标清电视等价的 luma 定义在一份名为 ITU-R BT.601 的规范中。对于高清电视,相关的规范是 ITU-R BT.709。

U 分量和 V 分量(也被称作饱和度(chroma)和色差(color difference)值),可以通过把原始 RGB 中的红绿分量从 Y 值中减去而得到:

U = Y’ – B

V = Y’ – R

这些值所包含的信息足够把 YUV 值再转换至原始 RGB 值。

YUV 的好处

由于部分历史原因,模拟电视使用 YUV。模拟彩色电视信号被设计成可以向前兼容黑白电视。彩色电视信号在 luma 信号之上叠加了色度信息(U 和 V)。黑白电视会忽略掉色度,把叠加的信号显示成灰度图像(刻意设计过的信号使得色度信号不会干扰到 luma 信号)。彩色电视机能够提取出色度信息从而把信号还原为 RGB。

YUV 还有另外一个跟今天主题更加相关的优点。比起色调的改变,人眼对亮度的改变更加敏感。所以,一幅图像可以携带比 luma 信息更少的色度信息而又不会以牺牲图像质量为代价。例如,比较通用的做法是把水平扫描线上的色度样本数降为 luma 样本数的一半。换句话说,对于一行像素上的每两个 luma 样本,只有一个 U 样本和一个 V 样本与之对应。假定每个值用 8 位来编码,那么每两个像素就需要 4 个字节的空间(两个 Y’,一个 U 和一个 V),这比同等的 24 位 RGB 编码要节约 30% 的空间或者说节约 16 位每像素。

YUV 并不是天生就比 RGB 更加密实。除非色度值被降低采样率,否则一个 YUV 像素跟一个 RGB 像素的尺寸是一样的。从 RGB 到 YUV 的转换是无损的。如果没有降低采样率,一个 YUV 像素可以被无损的转换回 RGB。降低采样率可以让 YUV 图像小一些,同时也会损失一些颜色信息。但是如果转换正确的话,这种损失是难以察觉的。

计算机视频中的 YUV

前面列出来用于 YUV 的公式并不能用来精确的转换数字视频。数字视频通常使用一种叫做 Y’CbCr 的 YUV。本质上,Y’CbCr 要把 YUV 分量缩放到如下取值范围内:

Component Range
Y’ 16-235
Cb/Cr 16-240,128 代表零

这些取值范围假定 Y’CbCr 分量的精度是 8 位。下面是 Y’CbCr 的准确来历,使用 BT.601 中定义的 luma:

  1. 我们把 RGB 的取值范围设定为 [0...1]。换句话说,纯黑是 0,纯白是 1。注意,这里说的是非线性(gamma corrected)RGB 值。

  2. 计算 luma。对于 BT.601,Y’ = 0.299R + 0.587G + 0.114B,参见先前的描述。

  3. 计算中间色度差值(B – Y’)和(R – Y’)。(B – Y’)的取值范围是 +/- 0.886,(R – Y’)是 +/- 0.701。

  4. 按如下方法缩放色度差值:

    Pb = (0.5 / (1 – 0.114)) × (B – Y’)

    Pr = (0.5 / (1 – 0.299)) × (R – Y’)

    这些缩放系数会赋予两个值相同的数值范围,+/- 0.5。它们定义的 YUV 颜色空间叫作 Y’PbPr,这个颜色空间被用在模拟视频分量上。

  5. 缩放 Y’PbPr 值以得到最终的 Y’CbCr值:

    Y’ = 16 + 219 × Y’

    Cb = 128 + 224 × Pb

    Cr = 128 + 224 × Pr

最后的这几个缩放系数用来把数值处理到之前的那个表格里列出的范围内。当然,你也可以不用存储中间结果而把 RGB 直接转换成 Y’CbCr。列在这里的步骤是为了展示 Y’CbCr 是如何从文章一开头的原始 YUV 演化而来的。

下面这个表格展示了不同颜色的 RGB 和 YCbCr 值,同样,这里使用的也是 BT.601 里定义的 luma。

Color R G B Y’ Cb Cr
Black 0 0 0 16 128 128
Red 255 0 0 81 90 240
Green 0 255 0 145 54 34
Blue 0 0 255 41 240 110
Cyan 0 255 255 170 166 16
Magenta 255 0 255 106 202 222
Yellow 255 255 0 210 16 146
White 255 255 255 235 128 128

在上面这张表中,Cb 和 Cr 没有在直觉上反映相应的颜色。例如纯白和纯黑都包含了一个中间值的 Cb 和 Cr(128)。Cb 的最高和最低值分别是蓝色和黄色。Cb 的最高和最低值分别是红色和青色。

报名学车啦

2010-3-8 22:32 | by 2ndboy

  本来去年的这个时候就想学的,但是太忙只好作罢,最近手上的项目进入稳定期,于是赶紧趁这个窗口期报了个名,接下来一两周要抽 5 个晚上的时间去上理论课了。不爽的是城西的上课地点居然只有西溪路上的公路技师学院一个,而且交通不怎么方便。

  一直以来感觉自己对机械操作还挺有感觉的,这是个检验的机会:D

FOURCC

2010-1-31 22:34 | by 2ndboy

  在 DirectShow 里面,各种 audio/video 的编码格式都可以用一个 GUID 来表示,比如 RGB24 的 GUID 是 MEDIASUBTYPE_RGB24(e436eb7d-524f-11ce-9f53-0020af0ba770)。但是在多媒体播放软件里(比如暴风影音),我们经常看到的是类似 AVC1、AAC2 这样的表示方法,这种用 4 个字符来表示的多媒体数据编码格式叫做 FourCC(Four-Character Codes)。

  FourCC 是一个 32 bits 的数值,基本上你可以把这个 32 bits 的值看作是 FourCC 里 4 个字符的 ASCII 码的组合,比如 YUY2 的 FourCC 是 0×32595559(Y=0×59, U=0×55, 2=0×32)。但并不是所有 FourCC 都符合这个规律,比如我上面举的 RGB24 就不是这样地:)

  FourCC 跟它对应的 GUID 之间是可以相互转换的,其实在 FourCC 后面加上 0000-0010-8000-00AA00389B71 就得到了它所对应的 GUID。比如 FourCC YUY2 所对应的 GUID MEDIASUBTYPE_YUY2 的值就是 {32595559-0000-0010-8000-00AA00389B71}。DirectShow 里面提供了一个工具类就是用来干这件事的:GUID g = (GUID)FOURCCMap( MAKEFOURCC(‘Y’,'U’,'Y’,'2′) );。

  FourCC 的分配是由 Microsoft 管理的,在这里可以找到一个已经注册的 FourCC 的列表,当然,不太全。

《True Colors》

2010-1-12 21:52 | by 2ndboy

  昨晚晚饭后在 VeryCD 上例行的逛逛,看到两张“新” iTunes Plus AAC 专辑,一张是 Dido 的《Life for Rent》,另外一张的封面造型比较前卫(是我不喜欢的那一型):

True Colors

  Erlend Bratland 的《True Colors》,歌者的名字没听说过。抱着聊胜于无的心态点了一首试听,马上惊为天人!看了看介绍,原来 2008 年 Erlend Bratland 在他 16 岁的时候就赢得了“Norway’s got talent”的冠军,果然实力不俗!

  今天专辑下载完以后马上转了一首专辑同名曲上来分享一下(原始 AAC 格式效果更好:)):

音频片段:需要 Adobe Flash Player(9 或以上版本)播放音频片段。 点击这里下载最新版本。您需要开启浏览器的 JavaScript 支持。

通过 IWMPGraphCreation 接口来定制 WMP 的 Filter Graph

2010-1-10 16:46 | by 2ndboy

  Windows Media Player 内部也是利用 DirectShow 来播放音视频内容的,这一点可以通过注册了 DirectShow Filter Graph SpyGraphEdit 来证实。那么有没有可能把 WMP 控件内嵌到我们自己的程序里,然后控制它的 Filter Graph 生成过程呢?答案就是 IWMPGraphCreation 接口。去年(其实是上个月;))研究了一下这个接口,最后终因版本要求过高,不适合用在我们的项目里而放弃使用,这里简单的做点笔记。

  根据 MSDN,这个接口只有在 WMP 10 及以上版本才支持。实现这个接口,跟 WMP 交互然后达到控制 WMP 的 Filter Graph 生成的步骤如下:

1) 首先要实现一个从 IOleClientSite, IWMPGraphCreation 和 IServiceProvider 这 3 个接口派生的类
2) 从 WMP 控件实例中取得 IOleObject 接口指针,然后调用 IOleObject::SetClientSite() 把我们新实现的类作为 WMP 的 site
3) WMP 会从它的 site 那里通过 QueryInterface() 得到 IServiceProvider 接口指针
4) 得到 IServiceProvider 接口以后 WMP 会调用 IServiceProvider::QueryService() 来尝试得到 IWMPGraphCreation 的接口指针
5) 在从我们这里得到 IWMPGraphCreation 接口指针以后,WMP 会调用我们的 IWMPGraphCreation::GetGraphCreationFlags(),这时我们需要给 flag 设置一个 WMPGC_FLAGS_USE_CUSTOM_GRAPH,表示我们想要定制 WMP 的 Filter Graph
6) WMP 控件在播放音视频文件之前会调用我们的 IWMPGraphCreation::GraphCreationPreRender() 实现,同时把它内部的 Filter Graph 接口指针传给我们,拿到 Filter Graph 指针以后我们就可以做我们想做的事情了

  最大的问题出在 step 5,WMPGC_FLAGS_USE_CUSTOM_GRAPH 这个 flag 在 WMP 12 及以上版本才支持,所以这个版本需求实在是有点太高了!另外一个意外是我的实测观察到,在 WMP 12 上面,step 5 及其后的动作,WMP 会做两遍,而不是我们直觉中的一遍,原因不明(Google 到一些资料,也有人遇到了这样的问题)。

犯错啦

2009-12-28 22:04 | by 2ndboy

  工作上的事,不管是多小的事,还是要再细心些、再仔细些!

  建立信誉是个长期艰巨的事儿,破坏信誉却太容易!

  不要怪运气不好,怪运气不好说明还有侥幸心理,做错了事就要承担后果/总结教训。

  犯低级错误对于完美主义者来说是种耻辱。

  鄙视一下落井下石的低素质“ren”,时不时要跳出来昭示一下你龌龊的手段。

  好了,也不要太在意一城一池的得失,站在圈子外面看的话,你也太不洒脱了!

年终总结 for 2009

2009-12-27 14:41 | by 2ndboy

  又到年底啦,来个年终总结吧,总结过去,展望未来:D

  今年生活上最大的变化就是装修和搬家,终于搬到自己的房子里住了,结束了长达 6 年收别人房租,自己又不用交房租的生活;) 虽然目前房子的位置不错(买菜乘车都很方便),但还是希望今后可以换个再大套一些的,小区环境好一些的。呵呵,不过目前杭州的这个房价实在是变态,这个愿望的实现还得再等等。

  工作方面,今年的状态那就是一个字——忙!个人感觉是从业以来最忙的一个年头,而且明年应该还会继续这样忙下去。工作之外应朋友之邀帮忙做了一些东西,所以忙上面还要再加一个累字,还好没有持续太长时间,否则可真要受不了了。虽然工作忙,但是今年在做的一个项目经历了一次比较大的调整,前期做的工作基本上等于是白忙了一场:( 最近正在从另外的方向做努力,累是累了点,但是由于接触的是以前不曾接触过的领域,所以感觉收获良多,学到的新东西也不少,从这方面讲,多少还有些安慰。

  身体方面,搬家之后周末经常会跟 LP 一起去爬山,运动量上应该说比以前是多了,不过从 US 回来以后积累的脂肪却一直不见少,看来还是要多运动,增强体力和精力。

  今年看了一些有关移民的资料和过来人经验,对移民有了更清醒的认识。虽然近几年没有移民的打算,但是这也许是几年以后的一个选项,所以英语方面,平时还要加强。

  新的一年,希望家人健康、平安、幸福。俺自己事业顺利,每天开心:D

聊聊 API hooking

2009-12-14 22:30 | by 2ndboy

  今天接着说说 API hook。API hook 大致上有两种手段:修改 IAT 和修改 API 指令。那种用一个自己写的同名 DLL 来替换目标 DLL 的手段就不提啦,可操作性太差。今天聊一下直接修改 API 指令这种方法。

  直接修改 API 指令,简单的说就是在目标 API 代码的开始处修改一些指令,让执行绪跳转到自己的代码里面。这里需要面对的问题有 3 个:如何找到 API 的代码地址,如何修改代码段,如何构造执行绪转移指令。

  前两个问题比较简单,无非是相关 API 的调用,用 GetProcAddress() 可以取到目标 API 的代码所在地址;用 VirtualProtect() 可以修改代码所在地址的访问权限,有了写权限之后就可以对目标 API 的代码做手脚了。

  至于如何构造执行绪转移指令,其实说白了就是在目标 API 代码的开始处放置一条无条件跳转指令 jmp。对于 32bits 系统,jmp 的机器码是 0xE9(这里不讨论短跳转 0xEB 的原因是通常我们自己代码跟目标 API 代码之间的偏移都大于 128),加上 32bits 的相对地址,一条无条件跳转指令的长度为 5 个字节。也就是说我们要至少覆盖目标 API 开始处的 5 个字节,但是直接覆盖别人的代码是肯定会出问题地,所以我们在覆盖目标 API 开始处指令之前要把这些指令搬到其它地方去,后面还有用。

  对于 jmp 指令后面那个相对地址,不是简单的拿目标地址减去当前 jmp 指令的地址就行,由于 CPU 要执行过 jmp 指令,拿到跳转地址再进行跳转,所以计算相对地址的时候要用“目的跳转地址 – ( jmp 所在指令地址 + 5 )”。目前为止,把对目标 API 的调用劫持到我们自己的函数这个目的已经达到了。但是我们还是得在需要的时候调用原始 API 来完成 caller 想要完成的功能,不过别忘了原始 API 已经被我们破坏掉了,所以调用原始 API 还要靠我们之前保存的 API 代码起始处的那些指令。

  假设我们刚好从目标 API 处复制了 5 个字节的完整指令(注意一定要是完整指令)放在内存 A 处,那么在 A + 5 处再构造一条跳转指令跳回到 API + 5 处就等于是把断成两截的 API 代码又连接了起来。这样一来,在需要调用原本的 API 功能时我们要调用的其实是 A。

以下是一些示例代码:

  1. typedef HRESULT (WINAPI *_DirectDrawCreate)( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter );
  2.  _DirectDrawCreate pOldDirectDrawCreate = NULL;
  3.  
  4.  HRESULT WINAPI
  5.  MyDirectDrawCreate( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter )
  6.  {
  7.      MessageBox( NULL, "MyDirectDrawCreate", "MyDirectDrawCreate", 0 );
  8.      return( pOldDirectDrawCreate( lpGUID, lplpDD, pUnkOuter) );
  9.  }
  10.  
  11.  int main( int argc, char *argv[] )
  12.  {
  13.      HMODULE hDll = LoadLibrary( "DDraw.dll" );
  14.      if( hDll )
  15.      {
  16.          pOldDirectDrawCreate = (_DirectDrawCreate)GetProcAddress( hDll, "DirectDrawCreate" );
  17.          HookAPI( (PVOID *)(&pOldDirectDrawCreate), MyDirectDrawCreate );
  18.      }
  19.  
  20.      LPDIRECTDRAW pDDraw = NULL;
  21.      HRESULT hr = DirectDrawCreate( NULL, &pDDraw, NULL );
  22.      if( DD_OK == hr )
  23.          hr = pDDraw->SetCooperativeLevel( NULL, DDSCL_NORMAL );
  24.  
  25.      return( 0 );
  26.  }

以上代码成功执行后,当我们调用 DirectDrawCreate() 的时候会被之前设置的 hook 函数截获,显示一个对话框。注意 HookAPI() 的第一个参数是指向指针的指针,所以我们开始时拿到的“原始” API 地址其实被换掉了(见上面描述,原始 API 代码已经被我们断成两截啦)。

下面是代码里用到的两个关键数据结构:

  1. #pragma pack (1)
  2.  struct HookTarget
  3.  {
  4.      char jmp;
  5.      int  address;
  6.  
  7.      HookTarget()
  8.      {
  9.          jmp = char( 0xE9 );
  10.      }
  11.  };
  12.  
  13.  struct HookThunk
  14.  {
  15.      char instructions[16];
  16.      HookTarget HookProc;
  17.  
  18.      HookThunk()
  19.      {
  20.          memset( instructions, 0x90, sizeof( instructions ) );
  21.      }
  22.  };
  23.  #pragma pack ()

HookTarget 是用来写覆盖在目标 API 起始处的无条件跳转指令的,HookThunk 是用来把断成两截的 API 代码连起来的。0×90 是 nop 的机器码,就算我们只从目标 API 那里复制了 5 个字节,后面的 nop 也会安全的让执行绪运行到 jmp 指令处。

下面是 HookAPI() 的代码:

  1. bool
  2.  HookAPI( PVOID *ppProc, LPCVOID pMyProc )
  3.  {
  4.      do
  5.      {
  6.          DWORD dwOldProtect = 0;
  7.          if( !RemoveReadOnly( *ppProc, 16, &dwOldProtect ) )
  8.              break;
  9.  
  10.          HookThunk *pThunk = new HookThunk;
  11.          int cbInstructions = CheckInstructions( (LPBYTE)*ppProc );
  12.          memcpy( pThunk, *ppProc, cbInstructions );
  13.          pThunk->HookProc.address = ( (int)*ppProc + cbInstructions )
  14.              - ( (int)pThunk + sizeof( HookThunk ) );
  15.  
  16.          DWORD dwTemp = 0;
  17.          if( !VirtualProtect( pThunk, sizeof( HookThunk ), dwOldProtect, &dwTemp ) )
  18.              break;
  19.  
  20.          HookTarget *pOriginalProc = (HookTarget *)*ppProc;
  21.          pOriginalProc->jmp     = char( 0xE9 );
  22.          pOriginalProc->address = (int)MyDirectDrawCreate - ( (int)pOriginalProc + 5 );
  23.  
  24.          if( !VirtualProtect( *ppProc, 16, dwOldProtect, &dwTemp ) )
  25.              break;
  26.  
  27.          *ppProc = pThunk;
  28.          return( true );
  29.      }
  30.      while( false );
  31.  
  32.      return( false );
  33.  }

就不详细解释了,看完了上面那一大坨原理分析,HookAPI() 这个函数很容易看懂。上面代码里面的 RemoveReadOnly() 就是对 VirtualProtect() 调用的包装而已,对于我们这个示例的目标 API——DirectDrawCreate(),CheckInstructions() 简单的返回 5 就可以了(如果程序执行异常,最好自己用调试器看一下指令边界在哪里)。

  注意以上的示例代码只是用来做测试之用,没有做完备的错误处理。而且在实际环境中目标 API 代码的起始指令边界不一定正好是 5 个字节,如果边界是 4 个字节和 6 个字节处,由于我们至少要覆盖 5 个字节,所以要多复制一些(复制 6 个字节)。另外代码中使用的指令集和地址都只适用于 32bits x86 系统,在其它 CPU 和 OS 上要做相应的修改才能正常运行。最后,在实际的环境中,还要写卸载 hook 的代码;)

Apache mod_rewrite 经验一则

2009-12-14 13:12 | by 2ndboy

  周末呼吸道发炎,今早起床发现还没好,所以在家休养。照理说这么个小毛病是不影响上班地,但是之前有过好几次类似的经验——呼吸道发炎,还去上班,结果在空气污浊的办公司待了一天,随后就严重起来啦!顺便在这儿鄙视一下公司办公室的空气,由于层高太低,电脑众多,通风不畅,基本上下午 2 点一过就头昏脑胀~~再说下去要坏了心情啦:D

  周末研究了一下 Apache 的 rewrite,这个东西有 RE 基础的人上手还是比较快的。不过期间遇到的小问题折腾了好久,我一开始试验的时候规则是放在 httpd.conf 里面的,后来移到 .htaccess 里面就没了效果,开始以为是没有开启对 .htaccess 的支持,后来经过大量折腾,才明白是规则写在 httpd.conf 和写在 .htaccess 里面,收到的 URL 不一样,比如访客用 http://www.foo.com/ 来访问,在 httpd.conf 里面拿到的 URL 串是“/”,而 .htaccess 里拿到的居然是“”,即便 .htaccess 所在目录的内容已经被配置成虚拟主机也一样,所以在 httpd.conf 里面的如下规则:

  1. RewriteRule /$ index.php?rewrite=1 [L]

放到 .htaccess 里面以后要改成:

  1. RewriteRule ^$ index.php?rewrite=1 [L]

  这就是有经验跟没经验的区别,浪费了时间,走了弯路换回来的经验,值得小记一下:)

DirectShow filter 体系初探

2009-12-6 13:11 | by 2ndboy

  再次工作上项目需要,要研究下 DirectShow 的 filter 体系,以及通过将自己实现的 filter 插入到播放 audio/video 时的 filter graph,最后达到获取进入 render filter 前的已经解码的 A/V 数据的目的。

  上周五利用下午时间已经找到了实现的方向,记录如下。其实网上公开的资料多如牛毛,现在的感受,不管是做哪方面的 research,只要不是高精尖到什么隐形飞行器涂料技术,你基本都可以在 Internet 上找到很多线索,有时候甚至是标准答案(所以说熟练运用搜索引擎很重要,这是快速学习的基础所在)。

  DirectShow 基于 COM 来构建,设计思想非常值得借鉴。在 DS 中发挥作用的组件都被称作 filter,每个 filter 有各自不同的作用,当它们被正确的连接在一起时就可以完成 audio/video 的回放。

  filter 这个名字起的不错,当多媒体数据流经一个个“过滤器”,经过 A/V 分离、decode 和 render 之后被最终呈现的用户面前。总体上说,有这么 3 类 filter:
1) source filter,负责读取源数据
2) transform filter,负责做各种数据变换
3) render filter,负责渲染结果

  每个 filter 都暴露出一到多个 pin,他们各自之间通过 pin 进行连接。source filter 只有 output pin,而 render filter 只有 input pin,transform filter 则既有 input 又有 output pin。当这些 filter 正确的通过各自的 pin 连接在一起时,就形成一个 filter graph。

  就拿用 DS 来播放一个 MP3 来说,filter graph 里会出现如下几个 filer:
一个 source filter,后面接一个 MPEG-I Stream Splitter(当然这只是一个 audio,所以不会分离出 video 数据来),然后是一个 MPEG Layer-3 Decoder,最末端是一个 Default DirectSound Device 的 render filter。
filter graph

  所以看来我们如果想在这中间插一杠子,把已经解码过的 A/V 数据拿出来的话,就需要开发一个 transform filter。经过一番 Google,发现我们要做的就是实现一个 Sample Grabber Filter,它本质上还是一个 transform filter,DS 架构里有这么一号的本意是为了在开发 transform filter 的过程中方便测试 transform filter 工作是否正常的,但 sample grabber filter 也同样非常适合抓取流经 filter graph 的数据!其实系统中是存在一个现成的 Sample Grabber Filter 的,我们要做的工作就是把这个 filter 加到 filter graph 里来,然后去实现一个 Sample Grabber Filter Callback 就可以拿到数据了。