本来想用TvideoCap的方法操作电脑摄像头,如以下是打开摄像头代码,能在XP和2003系统里能正常打开摄像头,但在win7里总是出各种问题,
const WM_CAP_START = WM_USER;
const WM_CAP_STOP = WM_CAP_START + 68;
const WM_CAP_DRIVER_CONNECT = WM_CAP_START + 10;
const WM_CAP_DRIVER_DISCONNECT = WM_CAP_START + 11;
const WM_CAP_SAVEDIB = WM_CAP_START + 25;
const WM_CAP_GRAB_FRAME = WM_CAP_START + 60;
const WM_CAP_SEQUENCE = WM_CAP_START + 62;
const WM_CAP_FILE_SET_CAPTURE_FILEA = WM_CAP_START + 20;
const WM_CAP_SEQUENCE_NOFILE =WM_CAP_START+ 63 ;
const WM_CAP_SET_OVERLAY =WM_CAP_START+ 51 ;
const WM_CAP_SET_PREVIEW =WM_CAP_START+ 50 ;
const WM_CAP_SET_CALLBACK_VIDEOSTREAM = WM_CAP_START +6;
const WM_CAP_SET_CALLBACK_ERROR=WM_CAP_START +2;
const WM_CAP_SET_CALLBACK_STATUSA= WM_CAP_START +3;
const WM_CAP_SET_CALLBACK_FRAME= WM_CAP_START +5;
const WM_CAP_SET_SCALE=WM_CAP_START+ 53 ;
const WM_CAP_SET_PREVIEWRATE=WM_CAP_START+ 52 ;
var
s:tpoint;
begin
s:=Pnl_View.ClientToScreen(point(0 ,0));
s:=ClientToScreen(point(Pnl_View.left,Pnl_View.Top));
hWndC := capCreateCaptureWindowA('My Own Capture Window',
WS_CHILD or WS_VISIBLE ,
Pnl_View.Left+3,Pnl_View.top+66,320,240,self.Handle,0);
if hWndC <> 0 then
SendMessage(hWndC, WM_CAP_SET_CALLBACK_VIDEOSTREAM, 0, 0);
SendMessage(hWndC, WM_CAP_SET_CALLBACK_ERROR, 0, 0);
SendMessage(hWndC, WM_CAP_SET_CALLBACK_STATUSA, 0, 0);
SendMessage(hWndC, WM_CAP_DRIVER_CONNECT, 0, 0);
SendMessage(hWndC, WM_CAP_SET_SCALE, 1, 0);
SendMessage(hWndC, WM_CAP_SET_PREVIEWRATE, 66, 0);
SendMessage(hWndC, WM_CAP_SET_OVERLAY, 1, 0);
SendMessage(hWndC, WM_CAP_SET_PREVIEW, 1, 0);
OpenVideo.Enabled :=false;
CloseVideo.Enabled:=true;
看到帖子上说"这种方式打开摄像头对摄像头的兼容性有要求,在XP试过很多,有的好用有的不好用,换过摄像头可能就好了。用DSPack控件,它是用Direct方式来做的,对摄像头的兼容性比较好。基本上手机、MP4、摄像头都可以用。"于是决定采用DSPack控件的方式。
DSPack,DSPack是一套使用微软Direct Show和DirectX技术的类和组件,设计工作于DirectX 9,支持系统Win9X, ME, 2000和Windows xp。大概看了下Direct Show和DirectX的百科,应该比较常用,而自己之前居然没有相关此方面的编程经验,实在惭愧,有机会有多补习才行。
源代码工程中有DSPack目录,原来前辈们用过它把码流转成Mpeg2的,可怜我今天才注意到这个控件啊:DSPack。
二. 安装
1 增加搜索路径 (DSPackDir)\src\DirectX9 和 (DSPackDir)\src\DSPack
在delphi6中选择菜单【Tools】-【Enviroment Options】,在打开的窗口中选择Library页签,在Library Path一项中添加这两个目录\DSPACK234\src\Directx9 ;\DSPACK234\src\DSPack
(将其直接粘贴在原有内容的后面,或者点击Library Path后面的...按钮添加)
2 编译 (DSPackDir)\packages\DirectX9_D6.dpk
双击(DSPackDir)\packages\DirectX9_D6.dpk,在delphi6中会显示一个关于重新创建资源文件的对话框,点击OK就可以了。
在delphi6中的打开窗口中点击compile按钮,完成编译。
在delphi6中选择菜单【File】-【Close All】,在提问是否保存时,选择保存。
3 编译 (DSPackDir)\packages\DSPack_D6.dpk
双击(DSPackDir)\packages\DSPack_D6.dpk,在delphi6中会显示一个关于重新创建资源文件的对话框,点击OK就可以了。
在delphi6中的打开窗口中,点击compile按钮,完成编译。
在delphi6中选择菜单【File】-【Close All】,在提问是否保存时,选择保存。
4 编译并安装 (DSPackDir)\packages\DSPackDesign_D6.dpk
双击(DSPackDir)\packages\DSPackDesign_D6.dpk,在delphi6中会显示一个关于重新创建资源文件的对话框,点击OK就可以了。
在delphi6中的打开窗口中,点击compile按钮,完成编译。 点击Install按钮,完成安装。
在delphi6中选择菜单【File】-【Close All】,在提问是否保存时,选择保存。
至此,安装已完成,在delphi的控件面板上可以找到DSPack的页签了。
(安装过程中遇到了一个编译错误,说找不到Jedi.inc。这个文件在src\Directx9目录下,为什么不能搜索。原来文档里写的路径是 (DSPackDir)\src\Directx9,而实际解压缩出来的目录名是DirectX9,所以添加搜索路径的时候一定要注意这个问题。把x改成X后,编译就通过了)
DSPack各种使用方法
一:用DSPack播放视频
首先,要阅读一下(DSPackDir)\help目录下的help.chm文件,粗略地看了一下,内容太多看不出头绪。
还是先学习一下(DSPackDir)\Demos\D6-D7目录下的那些例子,边动手做边学习吧。
研究的第一个例子是PlayWin。研究了一下,主要使用TFilterGraph和TVideoWindow来完成。
TFilterGraph是DSPack中的核心类,其他类都要围绕着它,但是怎么理解它还不清楚。TVideoWindow是个显示播放视频的控件。这两个类的关系好像是数据库控件中Dataset控件和DBGrid控件的关系一样。
看得差不多后,自己照葫芦画瓢仿制一个。
1. 新建一个应用,在界面上先放4、5个按钮。
2. 在控件面板上选择DSPack那页,把前两个控件(TFilterGraph和TVideoWindow)在窗口上各放一个。
3. 关联
选中VideoWindow1控件,在属性窗口中将FilterGraph属性设置为FilterGraph1。
选中FilterGraph1控件,确认属性窗口中的Mode属性为gmNormal。
4.为Form1增加一个onCreate事件处理程序。
内容为:
if not FilterGraph1.Active then FilterGraph1.Active := true;
FilterGraph1.ClearGraph;
FilterGraph1.RenderFile('E:\v\951.wmv'); // 简化一点,这里用你本地硬盘上的一个视频文件
5. 为Form1增加一个onCloseQuery事件处理程序。
内容为:
FilterGraph1.Active := false ;
6.把button1的Caption改为Start,并增加一个OnClick事件
内容为:
FilterGraph1.Play;
运行一下,就可以播放了。下面再增加几个功能按钮,如pause、stop。
7. 把button2的Caption改为Pause,并增加一个OnClick事件
内容为:
FilterGraph1.Pause;
8. 把button3的Caption改为Stop,并增加一个OnClick事件
内容为:
FilterGraph1.stop;
可以看出4-8步都是调用了TFilterGraph类的方法。
下面,再增加个全屏功能吧。
9. 把button4的Caption改为FullScreen,并增加一个OnClick事件
内容为:
VideoWindow1.FullScreen :=true ;
10. 为VideoWindow1增加一个OnClick事件
内容为:
if videowindow1.FullScreen then
videowindow1.FullScreen := false ; //退出全屏方式
一般的视频播放创窗口都有一个进度条,现在我们也来加一个。
A.1. 在DSPack控件面板上选择倒数第2个控件(TDSTrackBar),放到在窗口上。
A.2. 关联
选中DSTrackBar1控件,在属性窗口中将FilterGraph属性设置为FilterGraph1。(这一步好像很熟悉哦)
重新运行程序,你就会看到一个进度条,并且能够使用这一进度条来调整播放的进度。
二:使用DSPack打开摄像头
现在我们来看看(DSPackDir)\Demos\D6-D7目录下的PlayVideoCap,这是一个打开本机的视频输入设备的例子。
在这个例子中,又用到了一个新类:TFilter。
在我们依葫芦画瓢之前,你要装个摄像头或虚拟摄像头。虚拟摄像头可以用VCDCut、Softcam或Vcam等软件,也可以使用9158(http://www.9158.com/)或MVBox(http://www.mvbox.cn/)的虚拟视频。
先跟上次一样:
1. 新建一个应用,在界面上先放4、5个按钮,这次多放一个Listbox,这个列表框中将列出系统中安装的视频输入设备。
2. 在控件面板上选择DSPack那页,把前两个控件(TFilterGraph和TVideoWindow)在窗口上各放一个。
下面该有所不同了
3.在DSPack中选择TFilter控件,放到窗口上。
4.选中FilterGraph1控件,在属性窗口中将Mode属性设为gmCapture。
5. 关联
选中VideoWindow1控件,将FilterGraph属性设置为FilterGraph1。
选中Filter1控件,将FilterGraph属性设置为FilterGraph1。
6. 在代码模式中,在Interface后的Uses中增加
DSUtil, DirectShow9,
在implementation前面的Var中增加
SysDev: TSysDevEnum;
7. 为Form1增加一个onCreate事件处理程序,读取系统中的视频输入设备。
内容为:
var
i: integer;
begin
// 读取系统中的视频输入设备
SysDev:= TSysDevEnum.Create(CLSID_VideoInputDeviceCategory);
if SysDev.CountFilters > 0 then
for i := 0 to SysDev.CountFilters - 1 do
begin
Listbox1.Items.Add(SysDev.Filters[i].FriendlyName)
end;
end;
7. 同前。为Form1增加一个onCloseQuery事件处理程序。
内容为:
SysDev.Free;
FilterGraph1.ClearGraph;
FilterGraph1.Active := false ;
8. 为Listbox1增加一个onClick事件处理程序
内容为:
FilterGraph1.ClearGraph;
FilterGraph1.Active := false;
//设filter为所选视频输入设备
Filter1.BaseFilter.Moniker := SysDev.GetMoniker(Listbox1.ItemIndex);
FilterGraph1.Active := true;
// 打开所选的视频输入设备
with FilterGraph1 as ICaptureGraphBuilder2 do
RenderStream(@PIN_CATEGORY_PREVIEW, nil, Filter1 as IBaseFilter, nil, VideoWindow1 as IbaseFilter);
// 显示出来
FilterGraph1.Play;
运行一下试试,看是否能看到自己的摄像头的内容。
几个按钮没用,等到下一个例子用吧。
三:DSPack抓帧
在例子PlayVideoCap中,还有抓帧和回放的功能。这用到第4个控件TSampleGrabber。
我们以前两个例子为基础,增加抓帧的功能。
打开前面的例子,然后:
1. 在DSPack中选择TSampleGrabber控件,放到窗口上。然后将其FilterGraph属性设置为FilterGraph1。
2. 在选择一个标准控件TImage(在Additional页签中),放在窗口上。
3.讲一个没用的按钮的Caption改为"Snapshot",在它的OnClick事件中写:
SampleGrabber1.GetBitmap(Image1.Picture.Bitmap);
前面两个例子都可以这样增加抓帧功能。不过,对于第二个例子(即操作摄像头的例子),需要做额外的修改,就是:
将
RenderStream(@PIN_CATEGORY_PREVIEW, nil, Filter as IBaseFilter, nil, VideoWindow as IbaseFilter);
改为
RenderStream(@PIN_CATEGORY_PREVIEW, nil, Filter as IBaseFilter, SampleGrabber as IBaseFilter, VideoWindow as IbaseFilter);
这样就可以了
一开始总是不能保存录像文件,受到http://topic.csdn.net/t/20020519/11/734458.html 中这段话的启发,"在vfw中,在你显示设置属性(source,format,display)的时候,是不能够捕捉连接的.所以有你这个错误.而且在你在捕捉了以后,你也没有办法调用这几个设置(会返回错误). 一般是这样的:你先设置完成,再捕捉... 捕捉期间不能够运行设置,必须先停止.由于dshow设置这些东西也是调用vfw的,所以有同样的问题."明白原来是我在捕捉连接期间进行了设置,所以会返回错误,总是不能保存文件,修改为先停止设置完参数后再开始即可。
Delphi按钮控件最上层显示的方法:Button1.BringToFront;
摘录一些文章
DirectShow入门之Directshow的基本技巧
作者:任雪梅发表于 2011-12-3 11:58:22 评论(0) 阅读(266)
|
摘要:本文主要讲述了Directshow开发的一些基本概念和技巧,主要内容如下:
1、视频播放(Video Rendering)
2、如何处理事件通知(Event Notification)
3、如何枚举系统的设备和过虑器
4、如何枚举Graph图中的对象(filter,pin)
5、Seeking Filter graph
6、如何设置Graph时钟(Setting Graph Clock)
视频播放(Video Rendering)
dshow的视频提交过滤器可以在窗口模式和无窗口模式下工作。在窗口模式下,过滤器创建一个自己的窗口,在里面播放视频。在无窗口模式下,过滤器直接将视频在应用程序提供的窗口上显示,过滤器本身不创建窗口。
窗口模式
在窗口模式下,视频提交过滤器创建一个窗口,然后将视频祯帖到窗口上,你可以将这个窗口帖到你的应用程序的窗口。 Video Renderer只支持窗口模式,VMR-7 and VMR-9缺省的是窗口模式,也支持无窗口模式。
为了在你的应用程序中显示视频,你可以将视频窗口设置成应用程序的子窗口。你可以通过
IVideoWindow *pVidWin = NULL; pGraph->QueryInterface(IID_IVideoWindow, (void **)&g_pVidWin); pVidWin->put_Owner((OAHWND)hwnd); pVidWin->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS); RECT grc; GetClientRect(hwnd, &grc); pVidWin->SetWindowPosition(0, 0, grc.right, grc.bottom);
结束时一定要清理现场:
pControl->Stop(); pVidWin->put_Visible(OAFALSE); pVidWin->put_Owner(NULL);
无窗口模式
当采用无窗口的模式时,就没有必要暴露IVideoWindow接口了。
为了能够使用VMR的缺省行为,在构建Graph图之前必须要调整VMR。
1、创建一个过虑器图表管理器
2、创建一个VMR,加入到graph中
3、调用VMR的IVMRFilterConfig::SetRenderingMode方法设置VMRMode_Windowless标志。
4、调用IVMRWindowlessControl::SetVideoClippingWindow 给视频指定一个显示窗口。
然后调用IGraphBuilder::RenderFile或者其他的方法来创建其他的Graph。
下面的代码显示了如何创建一个VMR,将其添加到Graph,如何设置无窗口模式
HRESULT InitWindowlessVMR( HWND hwndApp, // Window to hold the video. IGraphBuilder* pGraph, // Pointer to the Filter Graph Manager. IVMRWindowlessControl** ppWc, // Receives a pointer to the VMR. ) { if (!pGraph || !ppWc) return E_POINTER; IBaseFilter* pVmr = NULL; IVMRWindowlessControl* pWc = NULL; // Create the VMR. HRESULT hr = CoCreateInstance(CLSID_VideoMixingRenderer, NULL, CLSCTX_INPROC, IID_IBaseFilter, (void**)&pVmr); if (FAILED(hr)) { return hr; }
// Add the VMR to the filter graph. hr = pGraph->AddFilter(pVmr, L"Video Mixing Renderer"); if (FAILED(hr)) { pVmr->Release(); return hr; } // Set the rendering mode. IVMRFilterConfig* pConfig; hr = pVmr->QueryInterface(IID_IVMRFilterConfig, (void**)&pConfig); if (SUCCEEDED(hr)) { hr = pConfig->SetRenderingMode(VMRMode_Windowless); pConfig->Release(); } if (SUCCEEDED(hr)) { // Set the window. hr = pVmr->QueryInterface(IID_IVMRWindowlessControl, (void**)&pWc); if( SUCCEEDED(hr)) { hr = pWc->SetVideoClippingWindow(hwndApp); if (SUCCEEDED(hr)) { *ppWc = pWc; // Return this as an AddRef'd pointer. } else { // An error occurred, so release the interface. pWc->Release(); } } } pVmr->Release(); return hr; }
你也可以调用下面的函数
IVMRWindowlessControl *pWc = NULL; hr = InitWindowlessVMR(hwnd, pGraph, &g_pWc); if (SUCCEEDED(hr)) { // Build the graph. For example: pGraph->RenderFile(wszMyFileName, 0); // Release the VMR interface when you are done. pWc->Release(); }
下面看看如何设置视频的位置
有两个矩形需要考虑,一个是源矩形,一个是目的矩形。源矩形决定开始播放视频的位置,目的矩形决定在窗口显示视频的区域。VMR将源矩形按照目的矩形的大小进行扩展。
IVMRWindowlessControl::SetVideoPosition可以设置两个矩形的大小,源矩形必须小于等于本地视频大小。你可以通过IVMRWindowlessControl::GetNativeVideoSize获取本地的视频区域大小。
// Find the native video size. long lWidth, lHeight; HRESULT hr = g_pWc->GetNativeVideoSize(&lWidth, &lHeight, NULL, NULL);
if (SUCCEEDED(hr)) { RECT rcSrc, rcDest; // Set the source rectangle. SetRect(&rcSrc, 0, 0, lWidth/2, lHeight/2);
// Get the window client area. GetClientRect(hwnd, &rcDest); // Set the destination rectangle. SetRect(&rcDest, 0, 0, rcDest.right/2, rcDest.bottom/2);
// Set the video position. hr = g_pWc->SetVideoPosition(&rcSrc, &rcDest); }
处理窗口消息
因为VMR没有自己的窗口,所以当视频需要重画或者改变的时候你要通知它。
1、当你接到一个WM_PAINT消息,你就要调用IVMRWindowlessControl::RepaintVideo来重画视频
2、当你接到一个WM_DISPLAYCHANGE消息,你就要调用IVMRWindowlessControl::DisplayModeChanged.
3、当你接到一个WM_SIZE消息时,重新计算视频的位置,然后调用SetVideoPostion。
下面的代码演示了WM_PAINT消息的处理:
void OnPaint(HWND hwnd) { PAINTSTRUCT ps; HDC hdc; RECT rcClient; GetClientRect(hwnd, &rcClient); hdc = BeginPaint(hwnd, &ps); if (g_pWc != NULL) { // Find the region where the application can paint by subtracting // the video destination rectangle from the client area. // (Assume that g_rcDest was calculated previously.) HRGN rgnClient = CreateRectRgnIndirect(&rcClient); HRGN rgnVideo = CreateRectRgnIndirect(&g_rcDest); CombineRgn(rgnClient, rgnClient, rgnVideo, RGN_DIFF);
// Paint on window. HBRUSH hbr = GetSysColorBrush(COLOR_BTNFACE); FillRgn(hdc, rgnClient, hbr); // Clean up. DeleteObject(hbr); DeleteObject(rgnClient); DeleteObject(rgnVideo); // Request the VMR to paint the video. HRESULT hr = g_pWc->RepaintVideo(hwnd, hdc); } else // There is no video, so paint the whole client area. { FillRect(hdc, &rc2, (HBRUSH)(COLOR_BTNFACE + 1)); } EndPaint(hwnd, &ps); }
尽管我们要自己处理onpaint消息,但是已经非常简单了。
如何处理事件通知(Event Notification)
当一个Directshow的应用程序运行的时候,在 filter Graph内部就会发生各种各样的事件,例如,一个filter也许发生数据流错误。Filter通过给graph mangaer发送事件通知来和graph通信,这个事件通知包括一个事件码和两个事件参数。事件码表示发生事件的类型,两个参数用来传递信息。
Filter发送的这些事件,其中的一部分可以被Manager直接处理,不通知应用程序,但有一部分事件,Manager将事件放入到一个队列中,等待应用程序处理。这里我们主要讨论在应用程序中经常遇到的三种事件
EC_COMPLETE表明回放已经结束
EC_USERABORT表明用户中断了回放。用户关闭视频播放窗口时,视频Render会发生这个事件
EC_ERRORABORT表明出现了一个错误。
应用程序可以通知filter graph manager,在某个指定的事件发生时,向指定的窗口发生一个指定的消息。这样应用程序就可以在消息循环中对发生的事件产生反应。
首先定义消息:
#define WM_GRAPHNOTIFY WM_APP + 1
然后向filter graph manager请求IMediaEventEx接口,然后调用IMediaEventEx::SetNotifyWindow方法来设置消息通知窗口:
IMediaEventEx *g_pEvent = NULL; g_pGraph->QueryInterface(IID_IMediaEventEx, (void **)&g_pEvent); g_pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);
然后在WindowProc函数增加一个处理WM_GRAPHNOTIFY消息的函数:
case WM_GRAPHNOTIFY: HandleGraphEvent(); break;
HandleGraphEvent()函数具体定义如下
void HandleGraphEvent() { // Disregard if we don't have an IMediaEventEx pointer. if (g_pEvent == NULL) { return; } // Get all the events long evCode; LONG_PTR param1, param2; HRESULT hr; while (SUCCEEDED(g_pEvent->GetEvent(&evCode, ?m1, ?m2,0))) { g_pEvent->FreeEventParams(evCode, param1, param2); switch (evCode) { case EC_COMPLETE: // Fall through. case EC_USERABORT: // Fall through. case EC_ERRORABORT: CleanUp(); PostQuitMessage(0); return; } } }
在释放IMediaEventEx指针前,要取消事件通知消息,代码如下:
// Disable event notification before releasing the graph. g_pEvent->SetNotifyWindow(NULL, 0, 0); g_pEvent->Release(); g_pEvent = NULL;
如何枚举系统的设备和过虑器
有时,应用程序需要查看系统中所有的filter。例如,视频应用程序需要列出系统中可用的捕捉设备。因为dshow基于com结构的,你在设计程序的时候是没法知道系统中正在使用的过滤器。Directshow提供了两种方法来枚举系统中注册的过虑器。
1、系统设备枚举器
系统设备枚举器提供了一个很好的方法根据种类来枚举系统中注册的过虑器。也许枚一种不同的硬件都会有自己的过虑器,或许所有的硬件设备共用同一个filter。这个对于采用WDM驱动程序的硬件很有用。
系统设备枚举器根据不同的种类创建了一个枚举器,例如,音频压缩,视频捕捉。不同种类的枚举器对于每一种设备返回一个独立的名称(moniker)。种类枚举器自动将相关的即插即用,演播设备包括进来。
按照下面的步骤使用设备枚举器:
1) 创建枚举器组件,CLSID为CLSID_SystemDeviceEnum
2) 指定某一种类型设备,参数CLSID,通过ICreateDevEnum::CreateClassEnumerator获取某一种类的枚举器,这个函数返回一个IEnumMoniker接口指针,如果该种类的空或者不存在,这个方法就返回S_FALSE。因此,当你调用这个函数时一定要检查返回值是否为S_OK,而不要用SUCCEEDED宏。
3) 然后IEnumMoniker::Next枚举每一个moniker。这个方法返回一个IMoniker接口指针。
4) 要想知道设备的名称,可以通过下面的函数IMoniker::BindToStorage
5) 然后利用IMoniker::BindToObject生成绑定道设备上的filter。调用IFilterGraph::AddFilter将filter添加到Graph图中。
// Create the System Device Enumerator. HRESULT hr; ICreateDevEnum *pSysDevEnum = NULL; hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void **)&pSysDevEnum); if (FAILED(hr)) { return hr; } // Obtain a class enumerator for the video compressor category. IEnumMoniker *pEnumCat = NULL; hr=pSysDevEnum->CreateClassEnumerator(CLSID_VideoCompressorCategory, &pEnumCat,0); if (hr == S_OK) { // Enumerate the monikers. IMoniker *pMoniker = NULL; ULONG cFetched; while(pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK) { IPropertyBag *pPropBag; hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pPropBag);//知道设备的名称 if (SUCCEEDED(hr)) { // To retrieve the filter's friendly name, do the following: VARIANT varName; VariantInit(&varName); hr = pPropBag->Read(L"FriendlyName", &varName, 0); if (SUCCEEDED(hr)) { // Display the name in your UI somehow. } VariantClear(&varName); // To create an instance of the filter, do the following: IBaseFilter *pFilter; hr = pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter,(void**)&pFilter); //生成一个filter绑定到设备上。 // Now add the filter to the graph. //Remember to release pFilter later. pPropBag->Release(); } pMoniker->Release(); } pEnumCat->Release(); } pSysDevEnum->Release();
在上面我们用IMoniker::BindToObject生成绑定道设备上的filter,当然我们还可以用另外的一种方法来生成绑定到设备上的filter 利用IMoniker::GetDisplayName得到moniker的名字。然后你把moniker的名字做参数传递给IFilterGraph2::AddSourceFilterForMoniker,就可以创建一个绑定到设备的filter了。在上面我们是调用IMoniker::BindToObject生成filter的,还是上面的简单些。看看代码吧。
LPOLESTR strName = NULL; IBaseFilter pSrc = NULL; hr = pMoniker->GetDisplayName(NULL, NULL, &strName); if (SUCCEEDED(hr)) { // Query the Filter Graph Manager for IFilterGraph2. IFilterGraph2 *pFG2 = NULL; hr = pGraph->QueryInterface(IID_IFilterGraph2, (void**)&pFG2); if (SUCCEEDED(hr)) { hr = pFG2->AddSourceFilterForMoniker(pMoniker, 0, L"Source", &pSrc); pFG2->Release(); } CoTaskMemFree(strName); } // If successful, remember to release pSrc.
2、Filter Mapper
搜索系统中的filter的另一个方法就是采用Filer Mapper。Filter mapper是一个com对象,它按照一定的条件来搜索系统的filer,它比系统设备枚举器(System Device Enumerator)的效率要低一些。所以当你要枚举某特定种类的filter时,你应该使用系统设备枚举器,但是当你搜索支持某种媒体类型的filter时,同时也找不到清晰的filter,你应该使用filter mapper。
Filter Mapper 暴露一个IFilerMapper2接口,要想搜索一个接口,你可以调用该接口的IFilterMapper2::EnumMatchingFilters方法,这个方法需要传递一些参数来定义搜索条件,同时该方法返回一个适合条件的filter的枚举器,这个枚举器提供一个IEnumMoniker接口,并且对于每个适合的filter都提供一个单独的moniker。
下面的例子演示了,枚举所有的支持DV,并且至少有一个输出pin的filter,这个filter支持任何媒体类型。
IFilterMapper2 *pMapper = NULL; IEnumMoniker *pEnum = NULL; hr =CoCreateInstance( CLSID_FilterMapper2,NULL, CLSCTX_INPROC, IID_IFilterMapper2, (void **) &pMapper); if (FAILED(hr)) { // Error handling omitted for clarity. } GUID arrayInTypes[2]; arrayInTypes[0] = MEDIATYPE_Video; arrayInTypes[1] = MEDIASUBTYPE_dvsd; hr = pMapper->EnumMatchingFilters( &pEnum, 0, // Reserved. TRUE, // Use exact match? MERIT_DO_NOT_USE+1, // Minimum merit. TRUE, // At least one input pin? 1, // Number of major type/subtype pairs for input. arrayInTypes, // Array of major type/subtype pairs for input. NULL, // Input medium. NULL, // Input pin category. FALSE, // Must be a renderer? TRUE, // At least one output pin? 0, // Number of major type/subtype pairs for output. NULL, // Array of major type/subtype pairs for output. NULL, // Output medium. NULL); // Output pin category. // Enumerate the monikers. IMoniker *pMoniker; ULONG cFetched; //////////下面就是枚举filter了,就是系统枚举设备filter
while (pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK) { IPropertyBag *pPropBag = NULL; hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pPropBag); if (SUCCEEDED(hr)) { // To retrieve the friendly name of the filter, do the following: VARIANT varName; VariantInit(&varName); hr = pPropBag->Read(L"FriendlyName", &varName, 0); if (SUCCEEDED(hr)) { // Display the name in your UI somehow. } VariantClear(&varName); // To create an instance of the filter, do the following: IBaseFilter *pFilter; hr = pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter, (void**)&pFilter); // Now add the filter to the graph. Remember to release pFilter later.
// Clean up. pPropBag->Release(); } pMoniker->Release(); } // Clean up. pMapper->Release(); pEnum->Release();
// Create the System Device Enumerator. HRESULT hr; ICreateDevEnum *pSysDevEnum = NULL; hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void **)&pSysDevEnum); if (FAILED(hr)) { return hr; } // Obtain a class enumerator for the video compressor category. IEnumMoniker *pEnumCat = NULL; hr=pSysDevEnum->CreateClassEnumerator(CLSID_VideoCompressorCategory, &pEnumCat,0); if (hr == S_OK) { // Enumerate the monikers. IMoniker *pMoniker = NULL; ULONG cFetched; while(pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK) { IPropertyBag *pPropBag; hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pPropBag);//知道设备的名称 if (SUCCEEDED(hr)) { // To retrieve the filter's friendly name, do the following: VARIANT varName; VariantInit(&varName); hr = pPropBag->Read(L"FriendlyName", &varName, 0); if (SUCCEEDED(hr)) { // Display the name in your UI somehow. } VariantClear(&varName); // To create an instance of the filter, do the following: IBaseFilter *pFilter; hr = pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter,(void**)&pFilter); //生成一个filter绑定到设备上。 // Now add the filter to the graph. //Remember to release pFilter later. pPropBag->Release(); } pMoniker->Release(); } pEnumCat->Release(); } pSysDevEnum->Release();
在上面我们用IMoniker::BindToObject生成绑定道设备上的filter,当然我们还可以用另外的一种方法来生成绑定到设备上的filter 利用IMoniker::GetDisplayName得到moniker的名字。然后你把moniker的名字做参数传递给IFilterGraph2::AddSourceFilterForMoniker,就可以创建一个绑定到设备的filter了。在上面我们是调用IMoniker::BindToObject生成filter的,还是上面的简单些。看看代码吧。
LPOLESTR strName = NULL; IBaseFilter pSrc = NULL; hr = pMoniker->GetDisplayName(NULL, NULL, &strName); if (SUCCEEDED(hr)) { // Query the Filter Graph Manager for IFilterGraph2. IFilterGraph2 *pFG2 = NULL; hr = pGraph->QueryInterface(IID_IFilterGraph2, (void**)&pFG2); if (SUCCEEDED(hr)) { hr = pFG2->AddSourceFilterForMoniker(pMoniker, 0, L"Source", &pSrc); pFG2->Release(); } CoTaskMemFree(strName); } // If successful, remember to release pSrc.
2、Filter Mapper
搜索系统中的filter的另一个方法就是采用Filer Mapper。Filter mapper是一个com对象,它按照一定的条件来搜索系统的filer,它比系统设备枚举器(System Device Enumerator)的效率要低一些。所以当你要枚举某特定种类的filter时,你应该使用系统设备枚举器,但是当你搜索支持某种媒体类型的filter时,同时也找不到清晰的filter,你应该使用filter mapper。
Filter Mapper 暴露一个IFilerMapper2接口,要想搜索一个接口,你可以调用该接口的IFilterMapper2::EnumMatchingFilters方法,这个方法需要传递一些参数来定义搜索条件,同时该方法返回一个适合条件的filter的枚举器,这个枚举器提供一个IEnumMoniker接口,并且对于每个适合的filter都提供一个单独的moniker。
下面的例子演示了,枚举所有的支持DV,并且至少有一个输出pin的filter,这个filter支持任何媒体类型。
IFilterMapper2 *pMapper = NULL; IEnumMoniker *pEnum = NULL; hr =CoCreateInstance( CLSID_FilterMapper2,NULL, CLSCTX_INPROC, IID_IFilterMapper2, (void **) &pMapper); if (FAILED(hr)) { // Error handling omitted for clarity. } GUID arrayInTypes[2]; arrayInTypes[0] = MEDIATYPE_Video; arrayInTypes[1] = MEDIASUBTYPE_dvsd; hr = pMapper->EnumMatchingFilters( &pEnum, 0, // Reserved. TRUE, // Use exact match? MERIT_DO_NOT_USE+1, // Minimum merit. TRUE, // At least one input pin? 1, // Number of major type/subtype pairs for input. arrayInTypes, // Array of major type/subtype pairs for input. NULL, // Input medium. NULL, // Input pin category. FALSE, // Must be a renderer? TRUE, // At least one output pin? 0, // Number of major type/subtype pairs for output. NULL, // Array of major type/subtype pairs for output. NULL, // Output medium. NULL); // Output pin category. // Enumerate the monikers. IMoniker *pMoniker; ULONG cFetched; //////////下面就是枚举filter了,就是系统枚举设备filter
while (pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK) { IPropertyBag *pPropBag = NULL; hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pPropBag); if (SUCCEEDED(hr)) { // To retrieve the friendly name of the filter, do the following: VARIANT varName; VariantInit(&varName); hr = pPropBag->Read(L"FriendlyName", &varName, 0); if (SUCCEEDED(hr)) { // Display the name in your UI somehow. } VariantClear(&varName); // To create an instance of the filter, do the following: IBaseFilter *pFilter; hr = pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter, (void**)&pFilter); // Now add the filter to the graph. Remember to release pFilter later.
// Clean up. pPropBag->Release(); } pMoniker->Release(); } // Clean up. pMapper->Release(); pEnum->Release();
3、查找媒体类型
每个pin都支持一个IPin::EnumMediaTypes方法,可以来枚举pin支持的媒体类型。它返回一个IEnumMediaTypes接口,这个接口的方法IEnumMediaTypes::Next返回一个指向AM_MEDIA_TYPE类型的指针。可以参考上面的代码来遍历pin所支持的媒体类型。
Seeking Filter graph
主要讲述了如何在一个媒体数据流中定位,任意指定开始播放的位置。
1、检查是否支持seek
Directshow通过IMediaSeeking接口支持seeking。Filter graph管理器支持这个接口,但是实际seeking的功能是有graph中的filter来实现的。
有一些数据是不能seek的,例如,你不可能seek从照相机中采集的活动的视频流。如果一个数据流可以被seek,但是,seek的类型还分以下几种类型,可以给你的数据流选择一种
1) 定位到数据流中的一个绝对位置
2) 返回数据流的持续时间
3) 返回数据流中的当前播放位置
4) 回放。
IMediaSeeking接口定义了一套标志AM_SEEKING_SEEKING_CAPABILITIES,用来描述可能支持的seek功能。
typedef enum AM_SEEKING_SeekingCapabilities { AM_SEEKING_CanSeekAbsolute = 0x1, AM_SEEKING_CanSeekForwards = 0x2, AM_SEEKING_CanSeekBackwards = 0x4, AM_SEEKING_CanGetCurrentPos = 0x8, AM_SEEKING_CanGetStopPos = 0x10, AM_SEEKING_CanGetDuration = 0x20, AM_SEEKING_CanPlayBackwards = 0x40, AM_SEEKING_CanDoSegments = 0x80, AM_SEEKING_Source = 0x100 } AM_SEEKING_SEEKING_CAPABILITIES;
可以通过IMediaSeeking::GetCapabilities查看数据流支持的seek能力都有哪些。应用程序可以采取 &测试每一项。例如,下面的代码检查了graph是否可以seek 一个任意的位置
DWORD dwCap = 0; HRESULT hr = pSeek->GetCapabilities(&dwCap); if (AM_SEEKING_CanSeekAbsolute & dwCap) { // Graph can seek to absolute positions. }
2、Setting and Retrieving the Position
Filter graph包含两个位置,当前位置和停止位置,定义如下:
1) 当前位置,当一个graph正处于运行的时候,当前位置就是当前的回放位置,相对于开始的位置而言。如果graph处于停止或者暂停状态的时候,当前位置就是数据流下次开始播放的位置点。
2) 停止位置,停止位置就是数据流将要停止的位置,当一个graph到达一个停止位置时,将没有数据流,filter graph管理器将会发送一个EC_COMPLETE事件。
可以通过IMediaSeeking::GetPositions方法可以获取这些位置值。返回值都是相对于原始的开始位置。
通过IMediaSeeking::SetPositions方法可以seek一个新的位置,见下面:
#define ONE_SECOND 10000000 REFERENCE_TIME rtNow = 2 * ONE_SECOND, rtStop = 5 * ONE_SECOND; hr = pSeek->SetPositions( &rtNow, AM_SEEKING_AbsolutePositioning, &rtStop, AM_SEEKING_AbsolutePositioning );
注:1秒是10,000,000参考时间单位。为了方便,这个例子将这个值定义为ONE_SECOND,如果你使用的dshow的基类,常量CUITS的值和这个值相等。
RtNow参数指定新的当前位置,第二个参数用来标示如何来定位rtNow参数。在这个例子中,AM_SEEKING_AbsolutePositioning 标志表示rtNow指定的位置是一个绝对的位置。RtStop参数指定了停止时间,最后一个参数也指定了绝对位置。
如果想指定一个相对的位置,可以指定一个AM_SEEKING_RelativePositioning参数,为了设置这个位置不能改变,可以指定一个AM_SEEKING_NoPositioning参数。此时,参考时间应该设置为NULL。下面的例子将位置向前seek 10秒,然后停止位置不变。
REFERENCE_TIME rtNow = 10 * ONE_SECOND; hr = pSeek->SetPositions( &rtNow, AM_SEEKING_RelativePositioning, NULL, AM_SEEKING_NoPositioning );
3、Setting the Playback Rate
调用IMediaSeeking::SetRate方法可以改变回放的速率。通过将新的速率设置成原来速率的倍数就可以设置新的速率,例如,pSeek->SetRate(2.0),将新的速率设置为原来速率的两倍。比率大于1说明回放的速度比原来的大,如果介于0和1之间,就比正常的速度慢。
如果我们不考虑回放速率,当前位置和停止位置相对于开始位置都是不变的。举个例子,如果我们有一个可以播放20秒的文件,将当前时间设置为10秒就会将播放位置设置到中间,如果播放的速率提高要原来的2倍,如果停止时间是20秒,你将播放位置设置到原来的10秒处,结果现在只能播放5秒了,因为速度提高了两倍。
4、Time Formats For Seek Commands
IMediaSeeking接口中的许多函数的参数都要求指定一个位置值,比如当前位置,或者停止位置,缺省的情况下这些参数是以of 100 nanoseconds为时间单位的,称为参考时间,任何支持seek的filter必须支持按参考时间来进行定位。一些filter也支持采取其他时间单位进行定位。例如,根据指定的桢的数量,或在数据流偏移的字节数进行定位。
这种用来定位的时间单位称为时间格式,采用一个GUID来标示。Directshow定义了一系列的时间格式,详细地可以参考SDK。第三方也可以定义自己的时间格式。
为了确定graph中的当前的filter是否支持特定的时间格式,可以调用 IMediaSeeking::IsFormatSupported方法,如果filter支持该时间格式,该函数返回ok否则返回false或者一个错误码。如果filter支持某种指定的时间格式,可以调用IMediaSeeking::SetTimeFormat方法切换到其他的时间格式。如果SetTimeFormat方法成功,下面的seek命令就要使用新的时间格式。
下面的代码检查graph是否支持用桢的数量进行定位,如果支持,定位到第20桢。
hr = pSeek->IsFormatSupported(&TIME_FORMAT_FRAME); if (hr == S_OK) { hr = pSeek->SetTimeFormat(&TIME_FORMAT_FRAME); if (SUCCEEDED(hr)) { // Seek to frame number 20. LONGLONG rtNow = 20; hr = pSeek->SetPositions(&rtNow, AM_SEEKING_AbsolutePositioning,0, AM_SEEKING_NoPositioning); } }
6、如何设置Graph时钟(Setting Graph Clock)
当你构建了一个graph后,graph管理器会自动地给你的graph选择一个参考时钟的。Graph中的所有filter都同步于时钟。特别的,Renderer filter还要根据参考时钟的时间来决定每一个sample的Presentation 时间。
通常的情况下,应用程序是没有必要重新设置graph管理器选择好的参考时钟的。但是,如果你想修改参考时钟,你可以通过graph管理器提供的IMediaFilter::SetSyncSource方法来重新设置参考时钟。这个方法的参数是一个时钟的IReferenceClock接口指针。可以在graph停止的时候调用这个函数,下面的例子演示了如何指定一个时钟
IGraphBuilder *pGraph = 0; IReferenceClock *pClock = 0; CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&pGraph); // Build the graph. pGraph->RenderFile(L"C:\\Example.avi", 0); // Create your clock. hr = CreateMyPrivateClock(&pClock); if (SUCCEEDED(hr)) { // Set the graph clock. IMediaFilter *pMediaFilter = 0; pGraph->QueryInterface(IID_IMediaFilter, (void**)&pMediaFilter); pMediaFilter->SetSyncSource(pClock); pClock->Release(); pMediaFilter->Release(); }
这段代码假定CreateMyPrivateClock 是应用程序定义的一个函数,用来创建一个时钟,然后返回一个IReferenceClock接口。
你也可以在graph没有设置时钟的情况下运行graph。当SetSyncSource 函数的参数为NULL的时候就给graph设置了一个空的参考时钟。如果graph没有时钟,graph将运行的快许多。因为renderer 不用再按照sample的presentation 时间了,只要sample到达了renderer filter,就可以立即被提交。所以,当你想处理数据尽可能快,而不是还要考虑预览的实际时间,你就可以给graph设置一个空的时间。 |
DSPack各种使用方法http://hi.baidu.com/zhangyihappy/blog/item/2038b28aa30ddbd8fd1f1085.html
一:用DSPack播放视频
首先,要阅读一下(DSPackDir)\help目录下的help.chm文件,粗略地看了一下,内容太多看不出头绪。 还是先学习一下(DSPackDir)\Demos\D6-D7目录下的那些例子,边动手做边学习吧。
研究的第一个例子是PlayWin。研究了一下,主要使用TFilterGraph和TVideoWindow来完成。 TFilterGraph是DSPack中的核心类,其他类都要围绕着它,但是怎么理解它还不清楚。TVideoWindow是个显示播放视频的控件。这两个类的关系好像是数据库控件中Dataset控件和DBGrid控件的关系一样。
看得差不多后,自己照葫芦画瓢仿制一个。
1. 新建一个应用,在界面上先放4、5个按钮。 2. 在控件面板上选择DSPack那页,把前两个控件(TFilterGraph和TVideoWindow)在窗口上各放一个。 3. 关联 选中VideoWindow1控件,在属性窗口中将FilterGraph属性设置为FilterGraph1。 选中FilterGraph1控件,确认属性窗口中的Mode属性为gmNormal。
4.为Form1增加一个onCreate事件处理程序。 内容为: if not FilterGraph1.Active then FilterGraph1.Active := true; FilterGraph1.ClearGraph; FilterGraph1.RenderFile('E:\v\951.wmv'); // 简化一点,这里用你本地硬盘上的一个视频文件
5. 为Form1增加一个onCloseQuery事件处理程序。 内容为: FilterGraph1.Active := false ;
6.把button1的Caption改为Start,并增加一个OnClick事件 内容为: FilterGraph1.Play;
运行一下,就可以播放了。下面再增加几个功能按钮,如pause、stop。
7. 把button2的Caption改为Pause,并增加一个OnClick事件 内容为: FilterGraph1.Pause;
8. 把button3的Caption改为Stop,并增加一个OnClick事件 内容为: FilterGraph1.stop;
可以看出4-8步都是调用了TFilterGraph类的方法。
下面,再增加个全屏功能吧。 9. 把button4的Caption改为FullScreen,并增加一个OnClick事件 内容为: VideoWindow1.FullScreen :=true ;
10. 为VideoWindow1增加一个OnClick事件 内容为: if videowindow1.FullScreen then videowindow1.FullScreen := false ; //退出全屏方式
一般的视频播放创窗口都有一个进度条,现在我们也来加一个。 A.1. 在DSPack控件面板上选择倒数第2个控件(TDSTrackBar),放到在窗口上。 A.2. 关联 选中DSTrackBar1控件,在属性窗口中将FilterGraph属性设置为FilterGraph1。(这一步好像很熟悉哦)
重新运行程序,你就会看到一个进度条,并且能够使用这一进度条来调整播放的进度。
二:使用DSPack打开摄像头
现在我们来看看(DSPackDir)\Demos\D6-D7目录下的PlayVideoCap,这是一个打开本机的视频输入设备的例子。
在这个例子中,又用到了一个新类:TFilter。
在我们依葫芦画瓢之前,你要装个摄像头或虚拟摄像头。虚拟摄像头可以用VCDCut、Softcam或Vcam等软件,也可以使用9158(http://www.9158.com/)或MVBox(http://www.mvbox.cn/)的虚拟视频。
先跟上次一样:
1. 新建一个应用,在界面上先放4、5个按钮,这次多放一个Listbox,这个列表框中将列出系统中安装的视频输入设备。 2. 在控件面板上选择DSPack那页,把前两个控件(TFilterGraph和TVideoWindow)在窗口上各放一个。
下面该有所不同了
3.在DSPack中选择TFilter控件,放到窗口上。 4.选中FilterGraph1控件,在属性窗口中将Mode属性设为gmCapture。 5. 关联 选中VideoWindow1控件,将FilterGraph属性设置为FilterGraph1。 选中Filter1控件,将FilterGraph属性设置为FilterGraph1。
6. 在代码模式中,在Interface后的Uses中增加 DSUtil, DirectShow9, 在implementation前面的Var中增加 SysDev: TSysDevEnum;
7. 为Form1增加一个onCreate事件处理程序,读取系统中的视频输入设备。 内容为: var i: integer; begin // 读取系统中的视频输入设备 SysDev:= TSysDevEnum.Create(CLSID_VideoInputDeviceCategory); if SysDev.CountFilters > 0 then for i := 0 to SysDev.CountFilters - 1 do begin Listbox1.Items.Add(SysDev.Filters[i].FriendlyName) end;
end;
7. 同前。为Form1增加一个onCloseQuery事件处理程序。 内容为: SysDev.Free; FilterGraph1.ClearGraph; FilterGraph1.Active := false ;
8. 为Listbox1增加一个onClick事件处理程序 内容为: FilterGraph1.ClearGraph; FilterGraph1.Active := false; //设filter为所选视频输入设备 Filter1.BaseFilter.Moniker := SysDev.GetMoniker(Listbox1.ItemIndex); FilterGraph1.Active := true; // 打开所选的视频输入设备 with FilterGraph1 as ICaptureGraphBuilder2 do RenderStream(@PIN_CATEGORY_PREVIEW, nil, Filter1 as IBaseFilter, nil, VideoWindow1 as IbaseFilter); // 显示出来 FilterGraph1.Play;
运行一下试试,看是否能看到自己的摄像头的内容。
几个按钮没用,等到下一个例子用吧。
三:DSPack抓帧
在例子PlayVideoCap中,还有抓帧和回放的功能。这用到第4个控件TSampleGrabber。
我们以前两个例子为基础,增加抓帧的功能。 打开前面的例子,然后:
1. 在DSPack中选择TSampleGrabber控件,放到窗口上。然后将其FilterGraph属性设置为FilterGraph1。 2. 在选择一个标准控件TImage(在Additional页签中),放在窗口上。 3.讲一个没用的按钮的Caption改为"Snapshot",在它的OnClick事件中写: SampleGrabber1.GetBitmap(Image1.Picture.Bitmap);
前面两个例子都可以这样增加抓帧功能。不过,对于第二个例子(即操作摄像头的例子),需要做额外的修改,就是: 将 RenderStream(@PIN_CATEGORY_PREVIEW, nil, Filter as IBaseFilter, nil, VideoWindow as IbaseFilter); 改为 RenderStream(@PIN_CATEGORY_PREVIEW, nil, Filter as IBaseFilter, SampleGrabber as IBaseFilter, VideoWindow as IbaseFilter);
这样就可以了 |
|
|