Delphi的DirectShow开发概述
第一部分:背景知识
DirectShow
是微软公司提供的一套在
Windows
平台上进行流媒体处理的开发包,与
DirectX
开发包一起发布。它经过
DirectX 6.0
中的
DirectX Media
发展而来,集成了
DirectX
家族中的其他成员(
DirectDraw
、
DirectSound
等),可以说是一位
“
集大成者
”
。
DirectShow
能做些什么?
DirectShow
为多媒体流的捕捉和回放提供了强有力的支持。运用
DirectShow
,可以很方便地从支持
WDM
驱动模型的采集卡上捕获数据,并且进行相应的后期处理乃至存储到文件中。它广泛地支持各种媒体格式,包括
Asf
、
Mpeg
、
Avi
、
Dv
、
Mp3
、
Wave
等等,使得多媒体数据的回放变得轻而易举。另外,
DirectShow
直接支持
DVD
的播放,视频的非线性编辑,以及与数字摄像机的数据交换。更值得一提的是,
DirectShow
提供的是一种开放式的开发环境,每个功能模块都采取
COM
组件方式,称为
Filter
,开发者也可以开发自己的功能
Filter
来扩展
DirectShow
的应用。按照功能来划分,
Filter
分为
3
类:
Source Filter, Transform Filter, Rendering Filter
。前者负责获取数据,数据源可以是文件、数字摄像机等,然后将数据往下传输;中间者负责数据的格式转换,比如数据流的分离与合成、编码解码等,然后把数据继续往下传输;后者负责数据的去向
——
给声卡、显卡进行播放或者输出到文件存储。
第二部分
核心技术
DirectShow
的开发实际就是
Filter
的开发,
DirectShow
自身提供了,下面就是
Filter
概述。
1
、
DirectShow
的
Filter
Filter 一般分为下面几种类型。
( 1 )源过滤器( source filter ):源过滤器引入数据到过滤器图表中,数据来源可以是文件、网络、照相机等。不同的源过滤器处理不同类型的数据源。
( 2 )变换过滤器( transform filter ):变换过滤器的工作是获取输入流,处理数据,并生成输出流。变换过滤器对数据的处理包括编解码、格式转换、压缩解压缩等。
( 3 )提交过滤器( renderer filter ):提交过滤器在过滤器图表里处于最后一级,它们接收数据并把数据提交给外设。
( 4 )分割过滤器( splitter filter ):分割过滤器把输入流分割成多个输出。例如, AVI 分割过滤器把一个 AVI 格式的字节流分割成视频流和音频流。
( 5 )混合过滤器( mux filter ):混合过滤器把多个输入组合成一个单独的数据流。例如, AVI 混合过滤器把视频流和音频流合成一个 AVI 格式的字节流。
过滤器的这些分类并不是绝对的,例如一个 ASF 读过滤器( ASF Reader filter )既是一个源过滤器又是一个分割过滤器。
2 、关于 Filter Graph Manager
Filter Graph Manager 也是一个 com 对象,用来控制 Filter graph 中的所有的 filter ,主要有以下的功能:
1) 用来协调 filter 之间的状态改变,从而使 graph 中的所有的 filter 的状态的改变应该一致。
2) 建立一个参考时钟。
3) 将 filter 的消息通知返回给应用程序
4) 提供用来建立 filter graph 的方法。
简单描述, Graph 就是各个 Filter 组成的一个流程图。
Filter 一般分为下面几种类型。
( 1 )源过滤器( source filter ):源过滤器引入数据到过滤器图表中,数据来源可以是文件、网络、照相机等。不同的源过滤器处理不同类型的数据源。
( 2 )变换过滤器( transform filter ):变换过滤器的工作是获取输入流,处理数据,并生成输出流。变换过滤器对数据的处理包括编解码、格式转换、压缩解压缩等。
( 3 )提交过滤器( renderer filter ):提交过滤器在过滤器图表里处于最后一级,它们接收数据并把数据提交给外设。
( 4 )分割过滤器( splitter filter ):分割过滤器把输入流分割成多个输出。例如, AVI 分割过滤器把一个 AVI 格式的字节流分割成视频流和音频流。
( 5 )混合过滤器( mux filter ):混合过滤器把多个输入组合成一个单独的数据流。例如, AVI 混合过滤器把视频流和音频流合成一个 AVI 格式的字节流。
过滤器的这些分类并不是绝对的,例如一个 ASF 读过滤器( ASF Reader filter )既是一个源过滤器又是一个分割过滤器。
2 、关于 Filter Graph Manager
Filter Graph Manager 也是一个 com 对象,用来控制 Filter graph 中的所有的 filter ,主要有以下的功能:
1) 用来协调 filter 之间的状态改变,从而使 graph 中的所有的 filter 的状态的改变应该一致。
2) 建立一个参考时钟。
3) 将 filter 的消息通知返回给应用程序
4) 提供用来建立 filter graph 的方法。
简单描述, Graph 就是各个 Filter 组成的一个流程图。
SourceFilter----|-----SpliterFilter-------------(Video-pin)>-----TransFormFilter--->VideoRender
|---------------------(Audio-pin)->----ACMWraperFilter--->DirectSoundFilter
程序启动过程,先创建各个
filter
的
com
对象,然后使用
FilterGraph.Addfilter
加入到
Graph
中,然后把每个
Filter
按照
数据流把
OutPin
和
inpuin
连接起来。最好启动
FilterGraph.play
即可。
Directshow 是基于模块化,每个功能模块都采取 COM 组件方式,称为 Filter 。 Directshow 提供了一系列的标准的模块可用于应用开发,开发者也可以开发自己的功能 Filter 来扩展 Directshow 的应用。下面我们用一个例子来说明如何采取 Filter 来播放一个 AVI 的视频文件。
1) 首先从一个文件中读取 AVI 数据,形成字节流。(这个工作由源 Filter 完成)
2) 检查 AVI 数据流的头格式,然后通过 AVI 分割 Filter 将视频流和音频流分开。
3) 解码视频流,根据压缩格式的不同,选取不同的 decoder filters 。
4) 通过 Renderer Filter 重画视频图像。
5) 音频流送到声卡进行播放,一般采用缺省的 DirectSound Device Filter 。
状态改变,
Graph
中的
filter
的状态改变应该一致,因此,应用程序并将状态改变的命令直接发给
filter
,而是将相应的状态改变的命令发送给
Filter graph Manager
,由
manager
将命令分发给
graph
中每一个
filter
。
Seeking
也是同样的方式工作,首先由应用程序将
seek
命令发送到
filter graph
管理器,然后由其分发给每个
filter
。
参考时钟, graph 中的 filter 都采用的同一个时钟,称为参考时钟( reference clock ),参考时钟可以确保所有的数据流同步,视频桢或者音频桢应该被提交的时间称为 presentation time.presentation time 是相对于参考时钟来确定的。 Filter graph Manager 应该选择一个参考时钟,可以选择声卡上的时钟,也可以选择系统时钟。
Graph 事件, Graph 管理器采用事件机制将 graph 中发生的事件通知给应用程序,这个机制类似于 windows 的消息循环机制。
Graph 构建的方法, graph 管理器给应用程序提供了将 filter 添加进 graph 的方法,连接 filter 的方法,断开 filter 连接的方法。
但是, graph 管理器没有提供如何将数据从一个 filter 发送到另一个 filter 的方法,这个工作是由 filter 在内部通过 pin 来独立完成的。
参考时钟, graph 中的 filter 都采用的同一个时钟,称为参考时钟( reference clock ),参考时钟可以确保所有的数据流同步,视频桢或者音频桢应该被提交的时间称为 presentation time.presentation time 是相对于参考时钟来确定的。 Filter graph Manager 应该选择一个参考时钟,可以选择声卡上的时钟,也可以选择系统时钟。
Graph 事件, Graph 管理器采用事件机制将 graph 中发生的事件通知给应用程序,这个机制类似于 windows 的消息循环机制。
Graph 构建的方法, graph 管理器给应用程序提供了将 filter 添加进 graph 的方法,连接 filter 的方法,断开 filter 连接的方法。
但是, graph 管理器没有提供如何将数据从一个 filter 发送到另一个 filter 的方法,这个工作是由 filter 在内部通过 pin 来独立完成的。
3
、媒体类型
因为 Directshow 是基于 com 组件的,就需要有一种方式来描述 filter graph 每一个点的数据格式,例如,我们还以播放 AVI 文件为例,数据以 RIFF 块的形式进入 graph 中,然后被分割成视频和音频流,视频流有一系列的压缩的视频桢组成,解压后,视频流由一系列的无压缩的位图组成,音频流也要走同样的步骤。
因为 Directshow 是基于 com 组件的,就需要有一种方式来描述 filter graph 每一个点的数据格式,例如,我们还以播放 AVI 文件为例,数据以 RIFF 块的形式进入 graph 中,然后被分割成视频和音频流,视频流有一系列的压缩的视频桢组成,解压后,视频流由一系列的无压缩的位图组成,音频流也要走同样的步骤。
媒体类型是一种很普遍的,可以扩展的用来描述数字媒体格式的方法,当两个
filter
连接的时候,他们会就采用某一种媒体类型达成一致的协议。媒体类型定义了处于源头的
filter
将要给下游的
filter
发送什么样的数据,以及数据的
physical layout
。如果两个
filter
不能够支持同一种的媒体类型,那么他们就没法连接起来。
对于大多数的应用来说,也许你不用考虑媒体类型,但是,有些应用程序中,你会直接应用到媒体类型的。
媒体类型是通过 AM_MEDIA_TYPE 结构定义的。
对于大多数的应用来说,也许你不用考虑媒体类型,但是,有些应用程序中,你会直接应用到媒体类型的。
媒体类型是通过 AM_MEDIA_TYPE 结构定义的。
Filters
通过
pin
的连接来传递数据,数据流是从一个
filter
的输出
pin
流向相连的
filter
的输入
pin
。输出
pin
常用的传递数据的方式是调用输入
pin
上的
IMemInputPin::Receive
方法。
对于
filter
来说,可以有好几种方式来分配媒体数据使用的内存块,可以在堆上分配,可以在
DirectDraw
的表面,也可以采用
GDI
共享内存,还有其他的一些方法,在
Directshow
中用来进行内存分配任务的是内存分配器(
allocator
),也是一个
COM
对象,暴露了一个
IMemAllocator
接口。
当两个 pin 连接的时候,必须有一个 pin 提供一个 allocator , Directshow 定义了一系列函数调用用来确定由哪个 pin 提供 allocator ,以及 buffer 的数量和大小。
在数据流开始之前, allocator 会创建一个内存池( pool of buffer ),在开始发送数据流以后,源 filter 就会将数据填充到内存池中一个空闲的 buffer 中,然后传递给下面的 filter 。但是,源 filter 并不是直接将内存 buffer 的指针直接传递给下游的 filter ,而是通过一个 media samples 的 COM 对象,这个 sample 是 allocator 创建的用来管理内存 buffer 。 Media sample 暴露了 IMediaSample 接口,一个 sample 包含了下面的内容:
一个指向没有发送的内存的指针。
一个时间戳
一些标志
媒体类型。
时间戳表明了 presentation time , Renderer filter 就是根据这个时间来安排 render 顺序的。标志是用来标示数据是否中断等等,媒体类型提供了中途改变数据格式的一种方法,不过,一般 sample 没有媒体类型,表明它们的媒体类型一直没有改变。
当一个 filter 正在使用 buffer ,它就会保持一个 sample 的引用计数, allocator 通过 sample 的引用计数用来确定是否可以重新使用一个 buffer 。这样就防止了 buffer 的使用冲突,当所有的 filter 都释放了对 sample 的引用, sample 才返回到 allocator 的内存池,供重新使用
当两个 pin 连接的时候,必须有一个 pin 提供一个 allocator , Directshow 定义了一系列函数调用用来确定由哪个 pin 提供 allocator ,以及 buffer 的数量和大小。
在数据流开始之前, allocator 会创建一个内存池( pool of buffer ),在开始发送数据流以后,源 filter 就会将数据填充到内存池中一个空闲的 buffer 中,然后传递给下面的 filter 。但是,源 filter 并不是直接将内存 buffer 的指针直接传递给下游的 filter ,而是通过一个 media samples 的 COM 对象,这个 sample 是 allocator 创建的用来管理内存 buffer 。 Media sample 暴露了 IMediaSample 接口,一个 sample 包含了下面的内容:
一个指向没有发送的内存的指针。
一个时间戳
一些标志
媒体类型。
时间戳表明了 presentation time , Renderer filter 就是根据这个时间来安排 render 顺序的。标志是用来标示数据是否中断等等,媒体类型提供了中途改变数据格式的一种方法,不过,一般 sample 没有媒体类型,表明它们的媒体类型一直没有改变。
当一个 filter 正在使用 buffer ,它就会保持一个 sample 的引用计数, allocator 通过 sample 的引用计数用来确定是否可以重新使用一个 buffer 。这样就防止了 buffer 的使用冲突,当所有的 filter 都释放了对 sample 的引用, sample 才返回到 allocator 的内存池,供重新使用
基于
Delphi
的
DirectShow
开发概述
2
MajorType
:
主要类型;例如视频,音频,还是位流
subType : 辅助说明类型,例如视频中的 YUV12 ,还是 UYVY 等等
formatType: 格式描述,更为细节的结构体。例如,视频大小,频率,帧率等 ,
可以使用 FORMAT_VIDEOINFO(VIDEOINFOHEADER) , FORMAT_WAVEFORMATEX(WAVEFORMATEX) 结构体来描述
//PAMMediaType
当使用 AM_MEDIA_TYPE 数据结构来描述媒体类型的时候,如果 MajorType , subType , formatType 都指定了 GUID ,那么
这就是完全媒体类型。
subType : 辅助说明类型,例如视频中的 YUV12 ,还是 UYVY 等等
formatType: 格式描述,更为细节的结构体。例如,视频大小,频率,帧率等 ,
可以使用 FORMAT_VIDEOINFO(VIDEOINFOHEADER) , FORMAT_WAVEFORMATEX(WAVEFORMATEX) 结构体来描述
//PAMMediaType
当使用 AM_MEDIA_TYPE 数据结构来描述媒体类型的时候,如果 MajorType , subType , formatType 都指定了 GUID ,那么
这就是完全媒体类型。
***************************************Filter
的连接
************************************
Filter 的连接实际上就是 Pin 的连接。连接方向总是由上一级的 Filter(UpStream Filter) 的输出 Pin 指向下一级
Filter ( DownStream Filter )的输入 Pin 。
Filter 的连接实际上就是 Pin 的连接。连接方向总是由上一级的 Filter(UpStream Filter) 的输出 Pin 指向下一级
Filter ( DownStream Filter )的输入 Pin 。
1.Filter
连接过程
Pin 也是一种 COM 接口。实现了 IPIN 的接口。一般通过调用 ( 下面的函数来连接 ) :
IFilterGraph.ConnectDirect , IGraphBuilder.Connect , IGraphBuilder.Render , IGraphBuilder.RenderFile
{ 下面就是个范例 ,一般 Filter 是在停止状态下连接的。
// 连接 source -> MPEG1Spliter
Source.FindPin(StringToOLEStr("Output"), OutPin);
MPEG1Splitter.FindPin(StringToOLEStr("Input"), inPin);
hr := (FilterGraph1 as IGraphBuilder).Connect(OutPin, InPin);
if FAILED(hr) then begin
ShowMessage("Failed connect mpg Source -> MPEG1Splitter");
exit;
end;
}
Pin 也是一种 COM 接口。实现了 IPIN 的接口。一般通过调用 ( 下面的函数来连接 ) :
IFilterGraph.ConnectDirect , IGraphBuilder.Connect , IGraphBuilder.Render , IGraphBuilder.RenderFile
{ 下面就是个范例 ,一般 Filter 是在停止状态下连接的。
// 连接 source -> MPEG1Spliter
Source.FindPin(StringToOLEStr("Output"), OutPin);
MPEG1Splitter.FindPin(StringToOLEStr("Input"), inPin);
hr := (FilterGraph1 as IGraphBuilder).Connect(OutPin, InPin);
if FAILED(hr) then begin
ShowMessage("Failed connect mpg Source -> MPEG1Splitter");
exit;
end;
}
2.FilterGraph
构建的方法
1)IFilterGraph.AddFilter 该参数提供一个 Filter 对象,将其加入到 FilterGraph 中 .
2)IFilterGraph.ConnectDirect 该参数提供输出 Pin ,输入 Pin 以及媒体类型,进行直接连接
3)IGraphBuilder.AddSourceFilter 该参数提供源文件名,自动将一个 SourceFilter 加载到 FilterGraph 中
4)IGraphBuilder.Connect 该参数提供输出 Pin ,输入 Pin 以及媒体类型,进行连接,如果失败,自动尝试在中间加入必要的格式转换 Filter
5)IGraphBuilder.Render 该参数提供输出 Pin, 自动间加入必要的 Filter 完成剩下的部分 FilterGraph 的构建(直到连到 RenderFilter 上)
6)IGraphBuilder.Render 该参数提供源文件名 , 自动间加入必要的 Filter 完成这个文件的回放
1)IFilterGraph.AddFilter 该参数提供一个 Filter 对象,将其加入到 FilterGraph 中 .
2)IFilterGraph.ConnectDirect 该参数提供输出 Pin ,输入 Pin 以及媒体类型,进行直接连接
3)IGraphBuilder.AddSourceFilter 该参数提供源文件名,自动将一个 SourceFilter 加载到 FilterGraph 中
4)IGraphBuilder.Connect 该参数提供输出 Pin ,输入 Pin 以及媒体类型,进行连接,如果失败,自动尝试在中间加入必要的格式转换 Filter
5)IGraphBuilder.Render 该参数提供输出 Pin, 自动间加入必要的 Filter 完成剩下的部分 FilterGraph 的构建(直到连到 RenderFilter 上)
6)IGraphBuilder.Render 该参数提供源文件名 , 自动间加入必要的 Filter 完成这个文件的回放
{
// 下面范例,表示该 FilterGraph 中有 6 个 Filter ,他们都是由 COM 对象创建而来。
var
Source : IBaseFilter;
MPEG1Splitter : IBaseFilter;
MpegVcodec : IBaseFilter;
AviDec : IBaseFilter;
AviDest : IBaseFilter;
Writer : IBaseFilter;
hr : HRESULT;
OutPin, InPin : IPin;
begin
CoCreateInstance(CLSID_AsyncReader, nil, CLSCTX_INPROC,IID_IBaseFilter, Source); // 典型的拉模式
CoCreateInstance(CLSID_MPEG1Splitter, nil, CLSCTX_INPROC,IID_IBaseFilter, MPEG1Splitter); //MPEG1 格式
CoCreateInstance(CLSID_CMpegVideoCodec, nil, CLSCTX_INPROC,IID_IBaseFilter, MpegVcodec); //MPEG 编码
CoCreateInstance(CLSID_AVIDec, nil, CLSCTX_INPROC,IID_IBaseFilter, AviDec); //AVI 解码
CoCreateInstance(CLSID_AviDest, nil, CLSCTX_INPROC, IID_IBaseFilter, AviDest); //AVI 目标
CoCreateInstance(CLSID_FileWriter, nil, CLSCTX_INPROC,IID_IBaseFilter, Writer); // 写文件
(FilterGraph1 as IFilterGraph).AddFilter(Source, "Source");
(FilterGraph1 as IFilterGraph).AddFilter(MPEG1Splitter, "MPEG1Splitter");
(FilterGraph1 as IFilterGraph).AddFilter(MpegVcodec, "MpegVcodec");
(FilterGraph1 as IFilterGraph).AddFilter(AviDec, "AviDec");
(FilterGraph1 as IFilterGraph).AddFilter(AviDest, "AviDest");
(FilterGraph1 as IFilterGraph).AddFilter(Writer, "Writer");
end;
}
3. 一般使用 GraphEdit 可以查看到目前正常安装在系统中的 Filter ,如果是安装在 DirectShow 目录下的可以通过指定 CLSID
用 CoCreateInstance 来创建。在其它目录下的,必须通过系统枚举来创建。
系统提供了一个 CLSID_SystemDeviceEnum ,用 CoCreateInstance 创建,并获取 ICreateDevEnum 接口。然后
使用 ICreateDevEnum.CreateClassEnumerator 为指定的类型目录创建一个枚举器,并获得 IEnumMoniker 接口。
使用 IEnumMoniker.next 方法,媒体该目录下所有可用设备标识( Device Moniker ) , 每个设备标识对象上都实现了 Imoniker 接口
调用 Imoniker.bindtoStorage 之后就可以访问设备标识属性集。比如得到设备的显示名字。
调用 Imoniker.BindToObject 可以将设备标识绑定成一个 DirecshowFilter ,然后调用 IFilterGraph.addFilter 加入 FilterGraph
参加工作。
DirectShow 通常有两个名字:显示名字例如: @device:cm:{33D9A760-90C8-11D0-BD43-00A0C911CE86}\xvid
友好名字例如: xvid mpeg4 decoder
{ 下面就是调用代码
var
i, j: integer;
AMoniker, MyMoniker: IMoniker;
PropBag: IPropertyBag;
AVariant: OleVariant;
CLSID: TGUID;
Found: boolean;
begin
for i := 0 to SysDevEnum.CountCategories - 1 do
cbCategories.Items.Add(SysDevEnum.Categories[i].FriendlyName); //SysDevEnum:TSysDevEnum;
Found := false;
j := 0;
MyMoniker := Filter.BaseFilter.Moniker;
if MyMoniker = nil then exit;
MyMoniker.BindToStorage(nil,nil,IPropertyBag, PropBag);
if PropBag.Read("CLSID",AVariant,nil) = S_OK then
CLSID := StringToGUID(AVariant)
else CLSID := GUID_NULL;
for i := 0 to SysDevEnum.CountCategories - 1 do
begin
SysDevEnum.SelectIndexCategory(i);
if SysDevEnum.CountFilters > 0 then
for j := 0 to SysDevEnum.CountFilters - 1 do
begin
if IsEqualGUID(CLSID, SysDevEnum.Filters[j].CLSID) then
begin
AMoniker := SysDevEnum.GetMoniker(j);
Found := AMoniker.IsEqual(MyMoniker) = S_OK;
AMoniker := nil;
end;
if Found then Break;
end;
if Found then
begin
cbCategories.ItemIndex := i;
cbCategoriesChange(nil);
lbFilters.ItemIndex := j;
lbFiltersClick(nil);
break;
end;
end;
PropBag := nil;
MyMoniker := nil;
}
PAMMediaType = ^TAMMediaType;
_AMMediaType = record
majortype : TGUID;
subtype : TGUID;
bFixedSizeSamples : BOOL;
bTemporalCompression : BOOL;
lSampleSize : ULONG;
formattype : TGUID;
pUnk : IUnknown;
cbFormat : ULONG;
pbFormat : Pointer;
end;
Filter 开发基础 ---- 基类分析
1 ) TBCBaseFilter
TBCBaseFilter = class(TBCUnknown, IBaseFilter, IAMovieSetup)
是最基本 Filter 的基类,使用方法:
( 1 ) 声明一个新类继承自 TBCBaseFilter
( 2 ) 在新类中定义一个 Filter 上 Pin 的实例。( Pin 从 TBCBasePin 继承)
( 3 ) 实现纯虚函数 TBCBaseFilter.GetPin, 用于返回 Filter 上各个 Pin 的对象指针
( 4 ) 实现纯虚函数 TBCBaseFilter.GetPinCount ,用于返回 Filter 上 Pin 的数量
( 5 ) 考虑如何处理从输入 Pin 进来的 Sample 数据
2 ) TBCBasePin
TBCBasePin 实现了 PIn 接口, TBCBasePin 设计了 Pin 的整个连接过程。也实现了 IQualityControl 质量控制接口。
在 TBCBasePin 上实现了 3 个成员函数与 Filter 状态对应。
( 1 ) Filter.Stopped <-------------> TBCBasePin.Inactive
( 2 ) Filter.Spaused <-------------> TBCBasePin.active
( 3 ) Filter.Running <-------------> TBCBasePin.Run
在实际开发 Filter 时,有可能重写该 3 个函数,用来初始化和释放必要资源。实现方法:
( 1 )从 TBCBasePin 派生一个子类
( 2 )实现纯虚函数 TBCBasePin.CheckMediaType, 进行 Pin 连接时媒体类型检测
( 3 )实现纯虚函数 TBCBasePin.GetMediaType ,提供 Pin 上的首先媒体类型
( 4 )实现 Ipin.BeginFlush 和 IPin.EndFlush 两个函数
( 5 )可能需要重写的函数包括。 TBCBasePin.Inactive,TBCBasePin.active,TBCBasePin.Run,
TBCBasePin.CheckConnect( 连接的时候检查,如查询对方 Pin 上是否支持某个特殊接口 ),
TBCBasePin.BreakConnect( 断开连接,并进行必要的资源释放 ),
TBCBasePin.CompleteConnect( 完成连接时被调用,可以在这个函数中获得当前连接用的媒体类型等参数 ),
TBCBasePin.EndOfStream( 当上流数据全部传送完毕后被调用,如果这是个 TransformFilter 则将继续往下送,
如是个 RenderFilter ,则需要向 FilterGraph 发送一个 EC_COMPLETE 事件 )
TBCBasePin.Notify( 直接响应质量控制,或者将质量控制消息往上一级 Filter 发送 )
3 ) TBCBaseInputPin 和 TBCBaseOutputPin
TBCBaseInputPin 和 TBCBaseOutputPin 都是从 TBCBasePin 派生而来,
TBCBaseInputPin 实现了 ImeminputPin (用于推模式的数据传送)
TBCBaseOutputPin 主要完成了传送数据所用的 Sample 管理器 (Allocate) 的协商,并重写了 TBCBasePin.active (用于
实际的 Sample 内存分配), TBCBasePin.inactvie (用于 Sample 内存的释放)。
TBCBaseInputPin 使用方法(派生一个子类,并且至少重写如下函数):
( 1 ) TBCBaseInputPin.BeginFlush
( 1 ) TBCBaseInputPin.EndFlush
( 1 ) TBCBaseInputPin.Receive
( 1 ) TBCBaseInputPin.CheckMediaType (一般在输出 Pin 上实现该函数)
( 1 ) TBCBaseInputPin.GetMediaType
TBCBaseOutputPin 使用方法(派生一个子类,并且至少重写如下函数):
( 1 )重写 TBCBasePin.CheckMediaType 进行连接时的媒体类型检查
( 2 )实现纯虚函数 TBCBaseOutputPin.DecideBufferSize ,决定 Sample 内存的大小
( 3 )重写纯虚函数 TBCBasePin.GetMediaType ,提供 Pin 上的首先媒体类型
4 ) TBCMediaType
TBCMediaType 用于数据传输的 Sample 的实现类, TBCMediaType 实现了 IMediaSample2 的接口, TBCMediaType 封装了
一个指向一块内存的指针,通过 TBCMediaType.GetPointer 得到
// 下面范例,表示该 FilterGraph 中有 6 个 Filter ,他们都是由 COM 对象创建而来。
var
Source : IBaseFilter;
MPEG1Splitter : IBaseFilter;
MpegVcodec : IBaseFilter;
AviDec : IBaseFilter;
AviDest : IBaseFilter;
Writer : IBaseFilter;
hr : HRESULT;
OutPin, InPin : IPin;
begin
CoCreateInstance(CLSID_AsyncReader, nil, CLSCTX_INPROC,IID_IBaseFilter, Source); // 典型的拉模式
CoCreateInstance(CLSID_MPEG1Splitter, nil, CLSCTX_INPROC,IID_IBaseFilter, MPEG1Splitter); //MPEG1 格式
CoCreateInstance(CLSID_CMpegVideoCodec, nil, CLSCTX_INPROC,IID_IBaseFilter, MpegVcodec); //MPEG 编码
CoCreateInstance(CLSID_AVIDec, nil, CLSCTX_INPROC,IID_IBaseFilter, AviDec); //AVI 解码
CoCreateInstance(CLSID_AviDest, nil, CLSCTX_INPROC, IID_IBaseFilter, AviDest); //AVI 目标
CoCreateInstance(CLSID_FileWriter, nil, CLSCTX_INPROC,IID_IBaseFilter, Writer); // 写文件
(FilterGraph1 as IFilterGraph).AddFilter(Source, "Source");
(FilterGraph1 as IFilterGraph).AddFilter(MPEG1Splitter, "MPEG1Splitter");
(FilterGraph1 as IFilterGraph).AddFilter(MpegVcodec, "MpegVcodec");
(FilterGraph1 as IFilterGraph).AddFilter(AviDec, "AviDec");
(FilterGraph1 as IFilterGraph).AddFilter(AviDest, "AviDest");
(FilterGraph1 as IFilterGraph).AddFilter(Writer, "Writer");
end;
}
3. 一般使用 GraphEdit 可以查看到目前正常安装在系统中的 Filter ,如果是安装在 DirectShow 目录下的可以通过指定 CLSID
用 CoCreateInstance 来创建。在其它目录下的,必须通过系统枚举来创建。
系统提供了一个 CLSID_SystemDeviceEnum ,用 CoCreateInstance 创建,并获取 ICreateDevEnum 接口。然后
使用 ICreateDevEnum.CreateClassEnumerator 为指定的类型目录创建一个枚举器,并获得 IEnumMoniker 接口。
使用 IEnumMoniker.next 方法,媒体该目录下所有可用设备标识( Device Moniker ) , 每个设备标识对象上都实现了 Imoniker 接口
调用 Imoniker.bindtoStorage 之后就可以访问设备标识属性集。比如得到设备的显示名字。
调用 Imoniker.BindToObject 可以将设备标识绑定成一个 DirecshowFilter ,然后调用 IFilterGraph.addFilter 加入 FilterGraph
参加工作。
DirectShow 通常有两个名字:显示名字例如: @device:cm:{33D9A760-90C8-11D0-BD43-00A0C911CE86}\xvid
友好名字例如: xvid mpeg4 decoder
{ 下面就是调用代码
var
i, j: integer;
AMoniker, MyMoniker: IMoniker;
PropBag: IPropertyBag;
AVariant: OleVariant;
CLSID: TGUID;
Found: boolean;
begin
for i := 0 to SysDevEnum.CountCategories - 1 do
cbCategories.Items.Add(SysDevEnum.Categories[i].FriendlyName); //SysDevEnum:TSysDevEnum;
Found := false;
j := 0;
MyMoniker := Filter.BaseFilter.Moniker;
if MyMoniker = nil then exit;
MyMoniker.BindToStorage(nil,nil,IPropertyBag, PropBag);
if PropBag.Read("CLSID",AVariant,nil) = S_OK then
CLSID := StringToGUID(AVariant)
else CLSID := GUID_NULL;
for i := 0 to SysDevEnum.CountCategories - 1 do
begin
SysDevEnum.SelectIndexCategory(i);
if SysDevEnum.CountFilters > 0 then
for j := 0 to SysDevEnum.CountFilters - 1 do
begin
if IsEqualGUID(CLSID, SysDevEnum.Filters[j].CLSID) then
begin
AMoniker := SysDevEnum.GetMoniker(j);
Found := AMoniker.IsEqual(MyMoniker) = S_OK;
AMoniker := nil;
end;
if Found then Break;
end;
if Found then
begin
cbCategories.ItemIndex := i;
cbCategoriesChange(nil);
lbFilters.ItemIndex := j;
lbFiltersClick(nil);
break;
end;
end;
PropBag := nil;
MyMoniker := nil;
}
PAMMediaType = ^TAMMediaType;
_AMMediaType = record
majortype : TGUID;
subtype : TGUID;
bFixedSizeSamples : BOOL;
bTemporalCompression : BOOL;
lSampleSize : ULONG;
formattype : TGUID;
pUnk : IUnknown;
cbFormat : ULONG;
pbFormat : Pointer;
end;
Filter 开发基础 ---- 基类分析
1 ) TBCBaseFilter
TBCBaseFilter = class(TBCUnknown, IBaseFilter, IAMovieSetup)
是最基本 Filter 的基类,使用方法:
( 1 ) 声明一个新类继承自 TBCBaseFilter
( 2 ) 在新类中定义一个 Filter 上 Pin 的实例。( Pin 从 TBCBasePin 继承)
( 3 ) 实现纯虚函数 TBCBaseFilter.GetPin, 用于返回 Filter 上各个 Pin 的对象指针
( 4 ) 实现纯虚函数 TBCBaseFilter.GetPinCount ,用于返回 Filter 上 Pin 的数量
( 5 ) 考虑如何处理从输入 Pin 进来的 Sample 数据
2 ) TBCBasePin
TBCBasePin 实现了 PIn 接口, TBCBasePin 设计了 Pin 的整个连接过程。也实现了 IQualityControl 质量控制接口。
在 TBCBasePin 上实现了 3 个成员函数与 Filter 状态对应。
( 1 ) Filter.Stopped <-------------> TBCBasePin.Inactive
( 2 ) Filter.Spaused <-------------> TBCBasePin.active
( 3 ) Filter.Running <-------------> TBCBasePin.Run
在实际开发 Filter 时,有可能重写该 3 个函数,用来初始化和释放必要资源。实现方法:
( 1 )从 TBCBasePin 派生一个子类
( 2 )实现纯虚函数 TBCBasePin.CheckMediaType, 进行 Pin 连接时媒体类型检测
( 3 )实现纯虚函数 TBCBasePin.GetMediaType ,提供 Pin 上的首先媒体类型
( 4 )实现 Ipin.BeginFlush 和 IPin.EndFlush 两个函数
( 5 )可能需要重写的函数包括。 TBCBasePin.Inactive,TBCBasePin.active,TBCBasePin.Run,
TBCBasePin.CheckConnect( 连接的时候检查,如查询对方 Pin 上是否支持某个特殊接口 ),
TBCBasePin.BreakConnect( 断开连接,并进行必要的资源释放 ),
TBCBasePin.CompleteConnect( 完成连接时被调用,可以在这个函数中获得当前连接用的媒体类型等参数 ),
TBCBasePin.EndOfStream( 当上流数据全部传送完毕后被调用,如果这是个 TransformFilter 则将继续往下送,
如是个 RenderFilter ,则需要向 FilterGraph 发送一个 EC_COMPLETE 事件 )
TBCBasePin.Notify( 直接响应质量控制,或者将质量控制消息往上一级 Filter 发送 )
3 ) TBCBaseInputPin 和 TBCBaseOutputPin
TBCBaseInputPin 和 TBCBaseOutputPin 都是从 TBCBasePin 派生而来,
TBCBaseInputPin 实现了 ImeminputPin (用于推模式的数据传送)
TBCBaseOutputPin 主要完成了传送数据所用的 Sample 管理器 (Allocate) 的协商,并重写了 TBCBasePin.active (用于
实际的 Sample 内存分配), TBCBasePin.inactvie (用于 Sample 内存的释放)。
TBCBaseInputPin 使用方法(派生一个子类,并且至少重写如下函数):
( 1 ) TBCBaseInputPin.BeginFlush
( 1 ) TBCBaseInputPin.EndFlush
( 1 ) TBCBaseInputPin.Receive
( 1 ) TBCBaseInputPin.CheckMediaType (一般在输出 Pin 上实现该函数)
( 1 ) TBCBaseInputPin.GetMediaType
TBCBaseOutputPin 使用方法(派生一个子类,并且至少重写如下函数):
( 1 )重写 TBCBasePin.CheckMediaType 进行连接时的媒体类型检查
( 2 )实现纯虚函数 TBCBaseOutputPin.DecideBufferSize ,决定 Sample 内存的大小
( 3 )重写纯虚函数 TBCBasePin.GetMediaType ,提供 Pin 上的首先媒体类型
4 ) TBCMediaType
TBCMediaType 用于数据传输的 Sample 的实现类, TBCMediaType 实现了 IMediaSample2 的接口, TBCMediaType 封装了
一个指向一块内存的指针,通过 TBCMediaType.GetPointer 得到
5
)
TBCSourceStream = class(TBCBaseOutputPin)
提供了 “ 推 ”(Push) 的能力,实现了一个线程( TBCAMThread ), Sample 数据就是靠这个线程向下一级 Filter 发送的。
实现方法:
( 1 )从 TBCSourceStream 派生一个子类作为 Pin
( 2 )实现纯虚函数 TBCSourceStream.CheckMediaType, 进行 Pin 连接时媒体类型检测
( 3 )实现纯虚函数 TBCSourceStream.GetMediaType ,提供 Pin 上的首先媒体类型
( 4 )实现 TBCBaseOutputPin.DecideBufferSize, 决定 Sample 内存大小,在 Pin 连接时会执行
( 5 )实现 TBCSourceStream.FillBuffer ,为即将传送出去的 Sample 填充数据
( 6 )可选地实现 TBCSourceStream.OnThreadCreate,
TBCSourceStream.OnThreadDestroy,
TBCSourceStream.OnThreadStartPlay 等函数,进行适当时机的初始化,资源管理等操作。
6 ) TBCTransformFilter
实现了媒体类型的转换,主要实现如下函数:
( 1 ) TBCTransformFilter.CheckInputType
( 2 ) TBCTransformFilter.CheckTransForm
( 3 ) TBCTransformFilter.DecideBufferSize
( 4 ) TBCTransformFilter.GetMediaType
( 5 ) TBCTransformFilter.TransForm
7)TBCTransinPlaceFilter
是一个 “ 就地 ” 处理的转换 Filter 。
8)TBCVideoTransformFilter
一个视频质量控制的基类。通过输入 Pin 上的 Receive 函数接收 Sample 时,能够根据质量消息决定是否丢帧,这个类
主要是为开发 AVI 解码 Filter 而设计的。使用方法基本和 TBCTransformFilter 相同。
9)TBCBaseRenderer
默认实现使用 TBCRenderInutPin 类的输入 Pin 。还实现了 IMediaSeeking 和 IMediaPosition 接口。使用方法:
( 1 )实现 TBCBaseRenderer.CheckMediaType ,用于检查输入 Pin 连接用的媒体类型
( 2 )实现 TBCBaseRenderer.DoRenderSample ,处理当前的 Sample
如果我们不处理 Sample ,需要写文件,基类可以选择从 TBaseFilter ,而此时输入 Pin 最好选择从 TBCRenderInputIn
派生类。
10)TBCBaseVideoRenderer
实现了 VideoFilter 基类,其中实现了 IQualityControl 用于视频质量控制, IQualProp 用于在 Filter 属性页显示一些
实时性能参数。使用方法与 TBCBaseRenderer 相同。
提供了 “ 推 ”(Push) 的能力,实现了一个线程( TBCAMThread ), Sample 数据就是靠这个线程向下一级 Filter 发送的。
实现方法:
( 1 )从 TBCSourceStream 派生一个子类作为 Pin
( 2 )实现纯虚函数 TBCSourceStream.CheckMediaType, 进行 Pin 连接时媒体类型检测
( 3 )实现纯虚函数 TBCSourceStream.GetMediaType ,提供 Pin 上的首先媒体类型
( 4 )实现 TBCBaseOutputPin.DecideBufferSize, 决定 Sample 内存大小,在 Pin 连接时会执行
( 5 )实现 TBCSourceStream.FillBuffer ,为即将传送出去的 Sample 填充数据
( 6 )可选地实现 TBCSourceStream.OnThreadCreate,
TBCSourceStream.OnThreadDestroy,
TBCSourceStream.OnThreadStartPlay 等函数,进行适当时机的初始化,资源管理等操作。
6 ) TBCTransformFilter
实现了媒体类型的转换,主要实现如下函数:
( 1 ) TBCTransformFilter.CheckInputType
( 2 ) TBCTransformFilter.CheckTransForm
( 3 ) TBCTransformFilter.DecideBufferSize
( 4 ) TBCTransformFilter.GetMediaType
( 5 ) TBCTransformFilter.TransForm
7)TBCTransinPlaceFilter
是一个 “ 就地 ” 处理的转换 Filter 。
8)TBCVideoTransformFilter
一个视频质量控制的基类。通过输入 Pin 上的 Receive 函数接收 Sample 时,能够根据质量消息决定是否丢帧,这个类
主要是为开发 AVI 解码 Filter 而设计的。使用方法基本和 TBCTransformFilter 相同。
9)TBCBaseRenderer
默认实现使用 TBCRenderInutPin 类的输入 Pin 。还实现了 IMediaSeeking 和 IMediaPosition 接口。使用方法:
( 1 )实现 TBCBaseRenderer.CheckMediaType ,用于检查输入 Pin 连接用的媒体类型
( 2 )实现 TBCBaseRenderer.DoRenderSample ,处理当前的 Sample
如果我们不处理 Sample ,需要写文件,基类可以选择从 TBaseFilter ,而此时输入 Pin 最好选择从 TBCRenderInputIn
派生类。
10)TBCBaseVideoRenderer
实现了 VideoFilter 基类,其中实现了 IQualityControl 用于视频质量控制, IQualProp 用于在 Filter 属性页显示一些
实时性能参数。使用方法与 TBCBaseRenderer 相同。
基于
Delphi
的
DirectShow
开发概述
3
Delphi
设计
Directshow
其实也是比较简单的,看了前面两个概述,相信你也会明白一些了,问题就是你要有什么杨的需求,然后根据需求来选择合适的
Filter
基类,从基类
(BassFilter)
继承下来后,只需要覆盖指定的函数就可以了。
例如我们要设计一个屏幕(
Desktop
)捕获的的
Filter
,每秒捕获大概
10
帧(
10
副抓屏
bitmap
),这个明显需要实现
Push
下推的功能,因此我们可以选择从
TBCSourceStream
基类派生一个子类,然后实现如下几个函数即可:
//GetMediaType
函数是下级
Filter
在和本
Filter
连接时在下级
InpuPin
接口上调用的
function GetMediaType(iPosition: Integer; out MediaType: PAMMediaType): HResult; override;
// 实现虚函数 CheckMediaType ,实现接受 8, 16, 24 or 32 RGB 位视频格式
// 如果媒体格式不能接受,则返回 E_INVALIDARG
function CheckMediaType(MediaType: PAMMediaType): HResult; override;
//
在本
Filter
的
OutPutPin
接口和下级
InputPin
接口协商时调用的,主要用来协商每个
Sample
的大小
function DecideBufferSize(Allocator: IMemAllocator;Properties: PAllocatorProperties): HRESULT; override;
function DecideBufferSize(Allocator: IMemAllocator;Properties: PAllocatorProperties): HRESULT; override;
//
设置视频媒体参数,在初始化
Filter
时调用
function SetMediaType(MediaType: PAMMediaType): HRESULT; override;
function SetMediaType(MediaType: PAMMediaType): HRESULT; override;
//
以上几个函数是在和下级
Filter
进行接口连接和协商时调用,
FillBuffer
函数则是由本
Filter
内置线程按一定时间
//
间隔调用,这里当然是把抓屏的
Bitmap
数据填充到
Sample
中,推给下一级。
function FillBuffer(Sample: IMediaSample): HResult; override;
function FillBuffer(Sample: IMediaSample): HResult; override;
// 实现虚函数,质量控制功能
function Notify(Filter: IBaseFilter; q: TQuality): HRESULT; override; stdcall;
具体代码分析如下:
CLSID_PushSourceDesktop: TGUID = "{570757C1-D2D8-42D1-BA0C-24E1BED3F62F}"; //PushFilter 注册名
//Pin 注册类型结构
TRegPinTypes = record
clsMajorType: PGUID;
clsMinorType: PGUID;
end;
//Setup 信息结构
sudPinTypes: TRegPinTypes =
(
// 视频流的类型
clsMajorType: @MEDIATYPE_Video;
// 所用可用类型
clsMinorType: @MEDIASUBTYPE_NULL
);
//Filter 注册 Pin 接口信息结构
TRegFilterPins = record
strName : PWideChar;
bRendered : BOOL;
bOutput : BOOL;
bZero : BOOL;
bMany : BOOL;
oFilter : PGUID;
strConnectsToPin : PWideChar;
nMediaTypes : LongWord;
lpMediaType : PRegPinTypes;
end;
// 定义实例
sudOutputPinDesktop: array[0..0] of TRegFilterPins =
(
(
strName: "Output"; // Pin 名称
bRendered: FALSE; // 是否是 Render
bOutput: TRUE; // 是否是输出接口
bZero: FALSE; // 是否允许为 0
bMany: FALSE; // 是否有更多
oFilter: nil; // 连接的 Filter
strConnectsToPin: nil; // 连接的 Pin
nMediaTypes: 1; // 支持类型数量
lpMediaType: @sudPinTypes // Pin 信息
)
);
DefaultFrameLength: TReferenceTime = FPS_10; // 由参考时钟确定 FPS_10=1000000
PushDesktopName: WideString = "_ PushSource Desktop Filter"; //PushFilter 友好名
//Pin 接口类封装,继承 TBCSourceStream<---TBCBaseOutputPin<----TBCBasePin(TBCUnknown, IPin, IQualityControl)
TBCPushPinDesktop = class(TBCSourceStream)
protected
FFramesWritten : Integer; // 在播放文件的时候跟踪当前位置
FZeroMemory : Boolean; // 是否必须清零 Buffer
FSampleTime : TRefTime; // 每个 Sample 一个时间戳
FFrameNumber : Integer; // 已经显示了多少帧了
FFrameLength : TReferenceTime; // 一帧的耗费时间
FScreenRect : TRect; // 包含需要捕获的视频框
FImageHeight, // 当前图像高
FImageWidth, // 当前图像宽
FRepeatTime, // 每帧之间重复时间 Time in msec between frames
FCurrentBitDepth: Integer; // 屏幕色彩位
FMediaType : TAMMediaType; // 媒体类型
FSharedState : TBCCritSec; // 临界区在资源共享中实现线程同步
public
constructor Create(out hr: HResult; Filter: TBCSource);
destructor Destroy; override;
// 实现虚函数,提供一个精确的媒体类型
function GetMediaType(iPosition: Integer; out MediaType: PAMMediaType): HResult; override;
// 实现虚函数,实现接受 8, 16, 24 or 32 RGB 位视频格式
// 如果媒体格式不能接受,则返回 E_INVALIDARG
function CheckMediaType(MediaType: PAMMediaType): HResult; override;
function DecideBufferSize(Allocator: IMemAllocator;Properties: PAllocatorProperties): HRESULT; override;
function SetMediaType(MediaType: PAMMediaType): HRESULT; override;
function FillBuffer(Sample: IMediaSample): HResult; override;
// 实现虚函数,质量控制功能
function Notify(Filter: IBaseFilter; q: TQuality): HRESULT; override; stdcall;
end;
//PushFilter 的类封装,继承自 TBCSource<-----TBCBaseFilter
// 把桌面抓屏图像作为连续视频流
TBCPushSourceDesktop = class(TBCSource)
private
FPin: TBCPushPinDesktop;
public
constructor Create(ObjName: string; Unk: IUnKnown; out hr: HRESULT);
constructor CreateFromFactory(Factory: TBCClassFactory; const Controller: IUnknown); override;
destructor Destroy; override;
end;
//TBCPushPinDesktop 析构函数
constructor TBCPushPinDesktop.Create(out hr: HResult; Filter: TBCSource);
var
DC: HDC;
begin
inherited Create("_ Push Source Desktop", hr, Filter, "Out");
FFramesWritten := 0;
FZeroMemory := False;
FFrameNumber := 0;
FFrameLength := FPS_5;
FSharedState := TBCCritSec.Create;
FCurrentBitDepth := 32;
CLSID_PushSourceDesktop: TGUID = "{570757C1-D2D8-42D1-BA0C-24E1BED3F62F}"; //PushFilter 注册名
//Pin 注册类型结构
TRegPinTypes = record
clsMajorType: PGUID;
clsMinorType: PGUID;
end;
//Setup 信息结构
sudPinTypes: TRegPinTypes =
(
// 视频流的类型
clsMajorType: @MEDIATYPE_Video;
// 所用可用类型
clsMinorType: @MEDIASUBTYPE_NULL
);
//Filter 注册 Pin 接口信息结构
TRegFilterPins = record
strName : PWideChar;
bRendered : BOOL;
bOutput : BOOL;
bZero : BOOL;
bMany : BOOL;
oFilter : PGUID;
strConnectsToPin : PWideChar;
nMediaTypes : LongWord;
lpMediaType : PRegPinTypes;
end;
// 定义实例
sudOutputPinDesktop: array[0..0] of TRegFilterPins =
(
(
strName: "Output"; // Pin 名称
bRendered: FALSE; // 是否是 Render
bOutput: TRUE; // 是否是输出接口
bZero: FALSE; // 是否允许为 0
bMany: FALSE; // 是否有更多
oFilter: nil; // 连接的 Filter
strConnectsToPin: nil; // 连接的 Pin
nMediaTypes: 1; // 支持类型数量
lpMediaType: @sudPinTypes // Pin 信息
)
);
DefaultFrameLength: TReferenceTime = FPS_10; // 由参考时钟确定 FPS_10=1000000
PushDesktopName: WideString = "_ PushSource Desktop Filter"; //PushFilter 友好名
//Pin 接口类封装,继承 TBCSourceStream<---TBCBaseOutputPin<----TBCBasePin(TBCUnknown, IPin, IQualityControl)
TBCPushPinDesktop = class(TBCSourceStream)
protected
FFramesWritten : Integer; // 在播放文件的时候跟踪当前位置
FZeroMemory : Boolean; // 是否必须清零 Buffer
FSampleTime : TRefTime; // 每个 Sample 一个时间戳
FFrameNumber : Integer; // 已经显示了多少帧了
FFrameLength : TReferenceTime; // 一帧的耗费时间
FScreenRect : TRect; // 包含需要捕获的视频框
FImageHeight, // 当前图像高
FImageWidth, // 当前图像宽
FRepeatTime, // 每帧之间重复时间 Time in msec between frames
FCurrentBitDepth: Integer; // 屏幕色彩位
FMediaType : TAMMediaType; // 媒体类型
FSharedState : TBCCritSec; // 临界区在资源共享中实现线程同步
public
constructor Create(out hr: HResult; Filter: TBCSource);
destructor Destroy; override;
// 实现虚函数,提供一个精确的媒体类型
function GetMediaType(iPosition: Integer; out MediaType: PAMMediaType): HResult; override;
// 实现虚函数,实现接受 8, 16, 24 or 32 RGB 位视频格式
// 如果媒体格式不能接受,则返回 E_INVALIDARG
function CheckMediaType(MediaType: PAMMediaType): HResult; override;
function DecideBufferSize(Allocator: IMemAllocator;Properties: PAllocatorProperties): HRESULT; override;
function SetMediaType(MediaType: PAMMediaType): HRESULT; override;
function FillBuffer(Sample: IMediaSample): HResult; override;
// 实现虚函数,质量控制功能
function Notify(Filter: IBaseFilter; q: TQuality): HRESULT; override; stdcall;
end;
//PushFilter 的类封装,继承自 TBCSource<-----TBCBaseFilter
// 把桌面抓屏图像作为连续视频流
TBCPushSourceDesktop = class(TBCSource)
private
FPin: TBCPushPinDesktop;
public
constructor Create(ObjName: string; Unk: IUnKnown; out hr: HRESULT);
constructor CreateFromFactory(Factory: TBCClassFactory; const Controller: IUnknown); override;
destructor Destroy; override;
end;
//TBCPushPinDesktop 析构函数
constructor TBCPushPinDesktop.Create(out hr: HResult; Filter: TBCSource);
var
DC: HDC;
begin
inherited Create("_ Push Source Desktop", hr, Filter, "Out");
FFramesWritten := 0;
FZeroMemory := False;
FFrameNumber := 0;
FFrameLength := FPS_5;
FSharedState := TBCCritSec.Create;
FCurrentBitDepth := 32;
//
这里关键是显示如何获取
DIB
图像,使用内存方式,把
DIB
图像插入到视频流
//
为了尽可能保持
Samle
采样,我们就需要从一个文件中读取图像,把它插入到发送下行接口流中
// 获取需要显示设备 context 上下文
DC := CreateDC("DISPLAY", nil, nil, nil);
// 获取需要显示设备 context 上下文
DC := CreateDC("DISPLAY", nil, nil, nil);
//
获取主桌面窗口的尺寸
FScreenRect.Left := 0;
FScreenRect.Top := 0;
FScreenRect.Right := GetDeviceCaps(DC, HORZRES);
FScreenRect.Bottom := GetDeviceCaps(DC, VERTRES);
FScreenRect.Left := 0;
FScreenRect.Top := 0;
FScreenRect.Right := GetDeviceCaps(DC, HORZRES);
FScreenRect.Bottom := GetDeviceCaps(DC, VERTRES);
//
保持该尺寸,为后面填充
Buffer
使用
FImageWidth := FScreenRect.Right - FScreenRect.Left;
FImageHeight := FScreenRect.Bottom - FScreenRect.Top;
FImageWidth := FScreenRect.Right - FScreenRect.Left;
FImageHeight := FScreenRect.Bottom - FScreenRect.Top;
//
释放资源
DeleteDC(DC);
DeleteDC(DC);
hr := S_OK;
end;
end;
destructor TBCPushPinDesktop.Destroy;
begin
{$IFDEF DEBUG}
DbgLog(self, Format("Frames written %d", [FFrameNumber]));
{$ENDIF}
inherited;
end;
begin
{$IFDEF DEBUG}
DbgLog(self, Format("Frames written %d", [FFrameNumber]));
{$ENDIF}
inherited;
end;
// 参考的视频格式, 8, 16 (*2), 24 or 32 bits per pixel
// 参考这些类型,选择更高的质量控制
// Therefore, iPosition =
// 0 Return a 32bit mediatype
// 1 Return a 24bit mediatype
// 2 Return 16bit RGB565
// 3 Return a 16bit mediatype (rgb555)
// 4 Return 8 bit palettised format
// >4 Invalid
{
PVideoInfo = ^TVideoInfo;
tagVIDEOINFO = record
rcSource: TRect; // 我们实际需要使用的位(在整个窗口中的 Sub 窗口)
rcTarget: TRect; // 该视频音频去哪
dwBitRate: DWORD; // 近似位率
dwBitErrorRate: DWORD; // 错位率
AvgTimePerFrame: TReferenceTime; // 每帧的平均时间 (100ns units)
bmiHeader: TBitmapInfoHeader; // 位图信息头,解码为 RGB 后可以形成一个 Bitmpa 图像
case Integer of
0: (
bmiColors: array[0..iPALETTE_COLORS-1] of TRGBQuad // 调色板
);
1: (
dwBitMasks: array[0..iMASK_COLORS-1] of DWORD // 真彩色掩码
);
2: (
TrueColorInfo: TTrueColorInfo // 两者都有
);
end;
PAMMediaType = ^TAMMediaType;
_AMMediaType = record
majortype : TGUID;
subtype : TGUID;
bFixedSizeSamples : BOOL;
bTemporalCompression : BOOL;
lSampleSize : ULONG;
formattype : TGUID;
pUnk : IUnknown;
cbFormat : ULONG;
pbFormat : Pointer;
end;
}
function TBCPushPinDesktop.GetMediaType(iPosition: Integer; out MediaType: PAMMediaType): HResult;
var
pvi: PVIDEOINFO;
i: Integer;
begin
FFilter.StateLock.Lock;
try
if (MediaType = nil) then
begin
Result := E_POINTER; // 指针错误
Exit;
end;
if (iPosition < 0) then
begin
Result := E_INVALIDARG; // 无效的位置
Exit;
end;
begin
Result := E_INVALIDARG; // 无效的位置
Exit;
end;
//
是否从类型结束处开始
Have we run off the end of types?
if (iPosition > 4) then
begin
Result := VFW_S_NO_MORE_ITEMS;
Exit;
end;
if (iPosition > 4) then
begin
Result := VFW_S_NO_MORE_ITEMS;
Exit;
end;
MediaType.cbFormat := SizeOf(TVideoInfo); //
视频类型
pvi := CoTaskMemAlloc(MediaType.cbFormat); // 为媒体类型结构体分配内存
if (pvi = nil) then
begin
Result := E_OUTOFMEMORY;
Exit;
end;
pvi := CoTaskMemAlloc(MediaType.cbFormat); // 为媒体类型结构体分配内存
if (pvi = nil) then
begin
Result := E_OUTOFMEMORY;
Exit;
end;
ZeroMemory(pvi, MediaType.cbFormat);
case iPosition of
0:
begin
// 返回 32 位格式的最高质量图像 =RGB888
pvi.bmiHeader.biCompression := BI_RGB; // 无压缩 RGB 格式
pvi.bmiHeader.biBitCount := 32;
end;
1:
begin
pvi.bmiHeader.biCompression := BI_RGB;
pvi.bmiHeader.biBitCount := 24;
end;
2:
begin
// 每像素 16 位 =RGB565 ,把 RGB 掩码以 DWord 类型,放入调色板前三个地方
for i := 0 to 2 do
pvi.TrueColorInfo.dwBitMasks[i] := bits565[i];
0:
begin
// 返回 32 位格式的最高质量图像 =RGB888
pvi.bmiHeader.biCompression := BI_RGB; // 无压缩 RGB 格式
pvi.bmiHeader.biBitCount := 32;
end;
1:
begin
pvi.bmiHeader.biCompression := BI_RGB;
pvi.bmiHeader.biBitCount := 24;
end;
2:
begin
// 每像素 16 位 =RGB565 ,把 RGB 掩码以 DWord 类型,放入调色板前三个地方
for i := 0 to 2 do
pvi.TrueColorInfo.dwBitMasks[i] := bits565[i];
pvi.bmiHeader.biCompression := BI_BITFIELDS;
pvi.bmiHeader.biBitCount := 16;
end;
3:
begin
// 每像素 16 位 =RGB555 ,把 RGB 掩码以 DWord 类型,放入调色板前三个地方
for i := 0 to 2 do
pvi.TrueColorInfo.dwBitMasks[i] := bits555[i];
pvi.bmiHeader.biBitCount := 16;
end;
3:
begin
// 每像素 16 位 =RGB555 ,把 RGB 掩码以 DWord 类型,放入调色板前三个地方
for i := 0 to 2 do
pvi.TrueColorInfo.dwBitMasks[i] := bits555[i];
pvi.bmiHeader.biCompression := BI_BITFIELDS;
pvi.bmiHeader.biBitCount := 16;
end;
4:
begin
// 每像素 8 位,可以不带调色板子
pvi.bmiHeader.biCompression := BI_RGB;
pvi.bmiHeader.biBitCount := 8;
pvi.bmiHeader.biClrUsed := iPALETTE_COLORS;
end;
end;
pvi.bmiHeader.biBitCount := 16;
end;
4:
begin
// 每像素 8 位,可以不带调色板子
pvi.bmiHeader.biCompression := BI_RGB;
pvi.bmiHeader.biBitCount := 8;
pvi.bmiHeader.biClrUsed := iPALETTE_COLORS;
end;
end;
//
任何视频格式都必须调整下面参数
pvi.bmiHeader.biSize := SizeOf(TBitmapInfoHeader);
pvi.bmiHeader.biWidth := FImageWidth;
pvi.bmiHeader.biHeight := FImageHeight;
pvi.bmiHeader.biPlanes := 1;
pvi.bmiHeader.biSizeImage := GetBitmapSize(@pvi.bmiHeader);
pvi.bmiHeader.biClrImportant := 0;
pvi.bmiHeader.biSize := SizeOf(TBitmapInfoHeader);
pvi.bmiHeader.biWidth := FImageWidth;
pvi.bmiHeader.biHeight := FImageHeight;
pvi.bmiHeader.biPlanes := 1;
pvi.bmiHeader.biSizeImage := GetBitmapSize(@pvi.bmiHeader);
pvi.bmiHeader.biClrImportant := 0;
//
清空源和目标框
SetRectEmpty(pvi.rcSource);
SetRectEmpty(pvi.rcTarget);
// 设置 Majortype 相关参数
MediaType.majortype := MEDIATYPE_Video; // 主类型为视频格式
MediaType.formattype := FORMAT_VideoInfo; // 格式类型为视频格式
MediaType.bTemporalCompression := False;
MediaType.bFixedSizeSamples := True;
SetRectEmpty(pvi.rcSource);
SetRectEmpty(pvi.rcTarget);
// 设置 Majortype 相关参数
MediaType.majortype := MEDIATYPE_Video; // 主类型为视频格式
MediaType.formattype := FORMAT_VideoInfo; // 格式类型为视频格式
MediaType.bTemporalCompression := False;
MediaType.bFixedSizeSamples := True;
//
设置
Subtype
相关参数
MediaType.subtype := GetBitmapSubtype(@pvi.bmiHeader);
MediaType.pbFormat := pvi;
MediaType.lSampleSize := pvi.bmiHeader.biSizeImage;
MediaType.subtype := GetBitmapSubtype(@pvi.bmiHeader);
MediaType.pbFormat := pvi;
MediaType.lSampleSize := pvi.bmiHeader.biSizeImage;
Result := S_OK;
finally
FFilter.StateLock.UnLock;
end;
end;
FFilter.StateLock.UnLock;
end;
end;
//
检查我们是否支持该媒体类型
function TBCPushPinDesktop.CheckMediaType(MediaType: PAMMediaType): HResult;
var
pvi: PVIDEOINFO;
SubType: TGUID;
begin
// 我们仅仅需要输出该视频
if not (IsEqualGUID(MediaType.majortype, MEDIATYPE_Video)) or
not (MediaType.bFixedSizeSamples) then
begin
Result := E_INVALIDARG;
Exit;
end;
function TBCPushPinDesktop.CheckMediaType(MediaType: PAMMediaType): HResult;
var
pvi: PVIDEOINFO;
SubType: TGUID;
begin
// 我们仅仅需要输出该视频
if not (IsEqualGUID(MediaType.majortype, MEDIATYPE_Video)) or
not (MediaType.bFixedSizeSamples) then
begin
Result := E_INVALIDARG;
Exit;
end;
//
检查我们支持的子类型
SubType := MediaType.subtype;
if IsEqualGUID(SubType, GUID_NULL) then
begin
Result := E_INVALIDARG;
Exit;
end;
SubType := MediaType.subtype;
if IsEqualGUID(SubType, GUID_NULL) then
begin
Result := E_INVALIDARG;
Exit;
end;
if not (
IsEqualGUID(SubType, MEDIASUBTYPE_RGB8) or
IsEqualGUID(SubType, MEDIASUBTYPE_RGB565) or
IsEqualGUID(SubType, MEDIASUBTYPE_RGB555) or
IsEqualGUID(SubType, MEDIASUBTYPE_RGB24) or
IsEqualGUID(SubType, MEDIASUBTYPE_RGB32)
) then
begin
Result := E_INVALIDARG;
Exit;
end;
IsEqualGUID(SubType, MEDIASUBTYPE_RGB8) or
IsEqualGUID(SubType, MEDIASUBTYPE_RGB565) or
IsEqualGUID(SubType, MEDIASUBTYPE_RGB555) or
IsEqualGUID(SubType, MEDIASUBTYPE_RGB24) or
IsEqualGUID(SubType, MEDIASUBTYPE_RGB32)
) then
begin
Result := E_INVALIDARG;
Exit;
end;
pvi := MediaType.pbFormat;
if (pvi = nil) then
begin
Result := E_INVALIDARG;
Exit;
end;
begin
Result := E_INVALIDARG;
Exit;
end;
//
检查图像尺寸是否改变,那么返回错误,以便重新调整图像大小
if (pvi.bmiHeader.biWidth <> FImageWidth) or
(abs(pvi.bmiHeader.biHeight) <> FImageHeight) then
begin
Result := E_INVALIDARG;
Exit;
end;
if (pvi.bmiHeader.biWidth <> FImageWidth) or
(abs(pvi.bmiHeader.biHeight) <> FImageHeight) then
begin
Result := E_INVALIDARG;
Exit;
end;
//
不接受负高度,超出屏幕外的不支持
if (pvi.bmiHeader.biHeight < 0) then
begin
Result := E_INVALIDARG;
Exit;
end;
if (pvi.bmiHeader.biHeight < 0) then
begin
Result := E_INVALIDARG;
Exit;
end;
Result := S_OK; //
接受该格式
end;
end;
// DecideBufferSize
:在视频格式协商好后,每次都必须调用它,确定需要传输的的内存大小
function TBCPushPinDesktop.DecideBufferSize(Allocator: IMemAllocator;Properties: PAllocatorProperties): HRESULT;
var
pvi: PVIDEOINFOHEADER;
Actual: ALLOCATOR_PROPERTIES;
begin
if (Allocator = nil) or (Properties = nil) then
begin
Result := E_POINTER;
Exit;
end;
function TBCPushPinDesktop.DecideBufferSize(Allocator: IMemAllocator;Properties: PAllocatorProperties): HRESULT;
var
pvi: PVIDEOINFOHEADER;
Actual: ALLOCATOR_PROPERTIES;
begin
if (Allocator = nil) or (Properties = nil) then
begin
Result := E_POINTER;
Exit;
end;
FFilter.StateLock.Lock;
try
pvi := AMMediaType.pbFormat; // 来自 TBasePin 接口的 AMMediaType
try
pvi := AMMediaType.pbFormat; // 来自 TBasePin 接口的 AMMediaType
Properties.cBuffers := 1; //
内存块数量
Properties.cbBuffer := pvi.bmiHeader.biSizeImage; // 内存块大小
Assert(Properties.cbBuffer <> 0); // 确保该 cbBuffer<>0, 否则会引发异常
Properties.cbBuffer := pvi.bmiHeader.biSizeImage; // 内存块大小
Assert(Properties.cbBuffer <> 0); // 确保该 cbBuffer<>0, 否则会引发异常
//
通过
allocator
分配查询我们需要的
Sample
内存大小
Result := Allocator.SetProperties(Properties^, Actual);
if Failed(Result) then Exit;
Result := Allocator.SetProperties(Properties^, Actual);
if Failed(Result) then Exit;
//
实际需要的分配的内存和属性中定义的不匹配
if (Actual.cbBuffer < Properties.cbBuffer) then
begin
Result := E_FAIL;
Exit;
end;
if (Actual.cbBuffer < Properties.cbBuffer) then
begin
Result := E_FAIL;
Exit;
end;
//
确保我们的实际内存块数量为
1
Assert(Actual.cBuffers = 1);
Result := S_OK;
Assert(Actual.cBuffers = 1);
Result := S_OK;
finally
FFilter.StateLock.UnLock;
end;
end;
FFilter.StateLock.UnLock;
end;
end;
// SetMediaType
,在两个
Filter
之间协商媒体类型时候,调用该函数
function TBCPushPinDesktop.SetMediaType(MediaType: PAMMediaType): HRESULT;
var
pvi: PVIDEOINFOHEADER;
begin
FFilter.StateLock.Lock;
try
// 通过基类传递设置
Result := inherited SetMediaType(MediaType);
function TBCPushPinDesktop.SetMediaType(MediaType: PAMMediaType): HRESULT;
var
pvi: PVIDEOINFOHEADER;
begin
FFilter.StateLock.Lock;
try
// 通过基类传递设置
Result := inherited SetMediaType(MediaType);
if Succeeded(Result) then
begin
pvi := AMMediaType.pbFormat;
begin
pvi := AMMediaType.pbFormat;
if (pvi = nil) then
begin
Result := E_UNEXPECTED;
Exit;
end;
begin
Result := E_UNEXPECTED;
Exit;
end;
// 8-bit palettized,
// RGB565, RGB555,
// RGB24,
// RGB32
if pvi.bmiHeader.biBitCount in [8, 16, 24, 32] then
begin
// 保存当前设定的媒体参数
FMediaType := MediaType^;
FCurrentBitDepth := pvi.bmiHeader.biBitCount;
end else
begin
// 除此之外不支持其它媒体类型
Assert(False);
Result := E_INVALIDARG;
end;
end;
// RGB565, RGB555,
// RGB24,
// RGB32
if pvi.bmiHeader.biBitCount in [8, 16, 24, 32] then
begin
// 保存当前设定的媒体参数
FMediaType := MediaType^;
FCurrentBitDepth := pvi.bmiHeader.biBitCount;
end else
begin
// 除此之外不支持其它媒体类型
Assert(False);
Result := E_INVALIDARG;
end;
end;
finally
FFilter.StateLock.UnLock;
end;
end;
FFilter.StateLock.UnLock;
end;
end;
//FillBuffer
是在每次采集视频数据后调用,把数据写入到
Stream
中,传递给下一个
Filter
// 这个函数中为虚抽象函数,所以必须实现代码完成实际的视频数据填充,以便下传
function TBCPushPinDesktop.FillBuffer(Sample: IMediaSample): HResult;
var
pData: PByte;
cbData: Longint;
hDib: HBitmap;
pvih: PVIDEOINFOHEADER;
Start, Stop: REFERENCE_TIME;
// 这个函数中为虚抽象函数,所以必须实现代码完成实际的视频数据填充,以便下传
function TBCPushPinDesktop.FillBuffer(Sample: IMediaSample): HResult;
var
pData: PByte;
cbData: Longint;
hDib: HBitmap;
pvih: PVIDEOINFOHEADER;
Start, Stop: REFERENCE_TIME;
function min(v1, v2: DWord): DWord;
begin
if v1 <= v2 then
Result := v1
else
Result := v2;
end;
begin
if v1 <= v2 then
Result := v1
else
Result := v2;
end;
begin
if (Sample = nil) then
begin
Result := E_POINTER;
Exit;
end;
if (Sample = nil) then
begin
Result := E_POINTER;
Exit;
end;
FSharedState.Lock;
try
// 获取 Sample 中视频数据内存指针( PData 获取)
Sample.GetPointer(pData);
cbData := Sample.GetSize;
try
// 获取 Sample 中视频数据内存指针( PData 获取)
Sample.GetPointer(pData);
cbData := Sample.GetSize;
//
确认我们将要访问的视频格式,如果不合格将出现异常
Assert(IsEqualGUID(AMMediaType.formattype, FORMAT_VideoInfo));
Assert(IsEqualGUID(AMMediaType.formattype, FORMAT_VideoInfo));
pvih := AMMediaType.pbFormat;
//
复制
DIB
位图数据到输出
Buffer
// 如果 Sample 的字节数大于真实图像字节,就限定复制的字节
pVih.bmiHeader.biSizeImage := min(pVih.bmiHeader.biSizeImage, cbData);
hDib := CopyScreenToBitmap(FScreenRect, pData, @pVih.bmiHeader);
// 如果 Sample 的字节数大于真实图像字节,就限定复制的字节
pVih.bmiHeader.biSizeImage := min(pVih.bmiHeader.biSizeImage, cbData);
hDib := CopyScreenToBitmap(FScreenRect, pData, @pVih.bmiHeader);
if (hDib <> 0) then DeleteObject(hDib);
//
设置时间戳,可以给帧率回调函数使用
// 如果这个是用 AVI 格式写的,那么我们就需要配置 AVI Mux Filter 来设定每帧的平均时间
// 当前时间
Start := FFrameNumber * FFrameLength;
Stop := Start + FFrameLength;
// 如果这个是用 AVI 格式写的,那么我们就需要配置 AVI Mux Filter 来设定每帧的平均时间
// 当前时间
Start := FFrameNumber * FFrameLength;
Stop := Start + FFrameLength;
Sample.SetTime(@Start, @Stop);
Inc(FFrameNumber);
Inc(FFrameNumber);
//
对于没有压缩的帧,都要设置为
True
,当作关键帧对待
Sample.SetSyncPoint(True);
Sample.SetSyncPoint(True);
Result := S_OK;
finally
FSharedState.UnLock;
end;
end;
FSharedState.UnLock;
end;
end;
//
返回质量控制,由于这时源头,所以可以收到来自
render
之间的各个
Filter
的
Notify
,
// 最终反应在 Filter (来自哪个), Q 质量控制参数,可以通过这两个参数来调整帧率和质量
function TBCPushPinDesktop.Notify(Filter: IBaseFilter; q: TQuality): HRESULT;
begin
Result := E_FAIL;
end;
// 最终反应在 Filter (来自哪个), Q 质量控制参数,可以通过这两个参数来调整帧率和质量
function TBCPushPinDesktop.Notify(Filter: IBaseFilter; q: TQuality): HRESULT;
begin
Result := E_FAIL;
end;
{
{$EXTERNALSYM CLSID_CVidCapClassManager} //BaseFilter 目录类型
//CLSID_LegacyAmFilterCategory: TGUID = (D1:$083863F1;D2:$70DE;D3:$11D0;D4:($BD,$40,$00,$A0,$C9,$11,$CE,$86));
}
initialization
TBCClassFactory.CreateFilter(TBCPushSourceDesktop, PushDesktopName,
CLSID_PushSourceDesktop, CLSID_LegacyAmFilterCategory,
MERIT_DO_NOT_USE, 1, @sudOutputPinDesktop
);
end.
TBCClassFactory.CreateFilter(TBCPushSourceDesktop, PushDesktopName,
CLSID_PushSourceDesktop, CLSID_LegacyAmFilterCategory,
MERIT_DO_NOT_USE, 1, @sudOutputPinDesktop
);
end.
//
由
FillBuffer
先填充
Sample
信息,然后由
DecideBufferSize
来真实的为视频数据分配内存,供下级
Filter
使用
// 加载顺序由下级 Filter 的 InputPin 接口调用本 OutPutPin 接口的 GetMediaType ,
// 然后调用 OutPutPin 接口的 SetMediaType 设定媒体格式,
// 调用 DecideBufferSize, 协商内存大小
// 启动后,不断调用 FillBuffer 填充 Sample
// 加载顺序由下级 Filter 的 InputPin 接口调用本 OutPutPin 接口的 GetMediaType ,
// 然后调用 OutPutPin 接口的 SetMediaType 设定媒体格式,
// 调用 DecideBufferSize, 协商内存大小
// 启动后,不断调用 FillBuffer 填充 Sample