基于directShow,打造全能播放器系列之一

总前言:我打算写一个能实现全能播放的播放器,功能比较简单,也算是抛砖引玉吧,因为内容较多,所以打算写三篇,这是开篇,欢迎大家吐槽

简易播放器的实现

本文的编写环境:visual studio 2008 ,基于MFC based DLG 的应用程序
前言:我写这个系列博客的目的,是想让大家知道,播放器的实现,其实没有想像的那么难,只是掌握了一点的方法,自己完全可以实现,当然出于容易讲解的目的,我会将代码写的尽量简洁,当然,在每个博客的最后都将贴出源代码地址,以供大家,研究学习。

前提:本文并不是假设你从零基础开始就能完全实现的,如果你根本没有了解过directShow,那还是请你从头开始吧,慢慢地了解个个函数的功能,然后再到这里来,因为我会从如何配置VS2005开始一步步的教到你完全写出这个播放器为止,但我并不会每句代码都会讲的很详细,如果有不理解的地方,你可以查看MSDN,directX SDK,找度娘,找谷歌,都是不错的选择。

注意:这篇博客与《DirectShow开发指南》第五章的例子,师从同路,高手可以不看

下面就开始播放器开发的旅程了,准备好了吧,那我们开始了

第一步:配置VS播放器,首先你得先安装directX 9.0开发包,安装好之后,记得编译dx9sdk\Samples\C++\DirectShow\BaseClasses这个目录下的baseclasses工程,然后就是将所需要的文件包含在VS2005配置中,为节省篇幅,这里就不再缀述,可以参看《DirectShow开发指南》P66-P67(开发环境的配置)

第二步:应用程序创建、界面及程序设置
创建一个MFC应用程序,based DLG,命名为Player

(一)配置
1、选择“项目”-》“player属性”打开属性页;
2、选择先在DEBUG模式下,选择“配置属性”-》“链接器”-》“输入”-》“附加依赖项”处添加:
strmbasd.lib uuid.lib winmm.lib Quartz.lib Strmiids.lib
如图:

3、然后将“配置”选项改为“Release”,重复上一操作,即添加相同的依赖项
4、在PlayDlg.cpp和PlayDlg.h的顶部加入directShow所需要的头文件#include

(二)界面设置
界面如下:

说明:
1、其中红框处,是添加的一个Picture Ctrl控件,ID号设置为:IDC_VIDEOWND,将其Type属性改为“Rectangle

2、添加了一个Slider Ctrl,其ID设置为IDC_PROGRESS

3、添加三个按钮,“打开”按钮的ID号为:IDC_BTN_OPEN,“播放”按钮的ID号设置为IDC_BTN_PLAY,“暂停”按钮的ID号设置为“IDC_BTN_PAUSE”,“停止”按钮的ID号设置为:IDC_BTN_STOP

(三)关联变量

1、对Picture Ctrl控件关联CStatic型变量m_VideoWindowPlay;

2、对Slider Ctrl控件关联CSliderCtrl型变量m_Slider;

注意:由于MFC本身的CSliderCtrl会存在很多问题,比如定位不准确,等,所以我们一般不使用这个类,而改成我们自己的类,请到下面的地下下载CNiceSlider

http://download.csdn.net/detail/harvic880925/4554013

然后将下载后的文件加载到工程中,并在PlayerDlg.h的文件中增加#include"NiceSlider.h"

然后将m_VideoWindow前的CStatic改为CNiceSliderCtrl,即

(四)初始化COM组件
因为DirectShowCOM组件,所以我们在使用前要先对其初始化,用完之后,也要手动解除

CPlayerApp类中的InitInstance()函数中,添加初始化代码:

HRESULT hr=CoInitialize(NULL);
if(FAILED(hr))
{
	printf("ERROR-Could not initialize COM libray");
	return -1;
}

位置如图:

然后添加在CPlayerApp类中添加ExitInstance()函数,在其中添加::CoUninitialize();以解除使用


第三步:实战

(一)变量定义、初始化及实例化
1、变量定义:

	IGraphBuilder * m_Graph;   //GraphBuilder对象,实现整个Graph的构建及执行等
	IMediaControl * m_MediaControl; //主要用来媒体控制,Run()、Pause()、Stop()等
 	IMediaEventEx * m_Event;     //主要用来关联消息接收及处理对象、实现消息处理,跟写WIN32 SDK程序差不多,需要自己捕捉消息,然后自定义处理函数
	IBasicVideo * m_BasicVideo;  //视频控制
	IBasicAudio * m_BasicAudio;  //音频控制
	IVideoWindow * m_VideoWindow;  //主要用来指定播放窗口,定义全屏,等
	IMediaSeeking * m_Seeking;   //主要用来媒体定位

以上只是对各变量功能作了下简单的讲解,如果想具体了解,可以查看SDKMSDN

2、初始化

CPlayerDlg::OnInitDialog()在添加初始化信息:

	m_Graph=NULL;
	 m_MediaControl=NULL;
	 m_Event=NULL;
	 m_BasicVideo=NULL;
	 m_BasicAudio=NULL;
	 m_VideoWindow=NULL;
	 m_Seeking=NULL;
	this->m_Slider.SetRange(0,1000);
	this->m_Slider.SetPos(0);

3、实例化

在CPlayerDlg类中,添加一个函数Create();专门用来实例化各个变量

bool CPlayerDlg::Create()
{
	if(!m_Graph)
	{
		HRESULT hr=S_OK;
		if(SUCCEEDED(::CoCreateInstance(CLSID_FilterGraph,NULL,CLSCTX_INPROC_SERVER,IID_IGraphBuilder,(void * *)&m_Graph)))
		{
			hr |=this->m_Graph->QueryInterface(IID_IMediaControl,(void **)&this->m_MediaControl);
			hr |=this->m_Graph->QueryInterface(IID_IBasicAudio,(void **)&this->m_BasicAudio);
			hr |=this->m_Graph->QueryInterface(IID_IMediaEventEx,(void **)&this->m_Event);
			hr |=this->m_Graph->QueryInterface(IID_IBasicVideo,(void **)&this->m_BasicVideo);
			hr |=this->m_Graph->QueryInterface(IID_IVideoWindow,(void **)&this->m_VideoWindow);
			hr |=this->m_Graph->QueryInterface(IID_IMediaSeeking,(void **)&this->m_Seeking);

			if(this->m_Seeking)
			{
				m_Seeking->SetTimeFormat(&TIME_FORMAT_MEDIA_TIME);//将时间设置为以100ns为单位的格式
			}
			return SUCCEEDED(hr);
		}
		m_Graph=0;
	}
	return false;
}

(二)针对更接口函数的封装

1、针对IMediaControl类的函数封装,封装Run(),Stop,Pause()函数,及IsRun(),IsStop(),IsPause()函数的实现,代码简单,不再详述

bool CPlayerDlg::IsRunning()
{
	if(m_Graph&&this->m_MediaControl)
	{
		OAFilterState FilterState=State_Stopped;
		HRESULT hr=this->m_MediaControl->GetState(10,&FilterState);
		if(SUCCEEDED(hr))
		{
			if(FilterState==State_Running)
			{return true;}
		}
	}
	return false;
}
bool CPlayerDlg::Run()
{
	if(m_Graph&&this->m_MediaControl)
	{
		if(!IsRunning())
		{
			if(SUCCEEDED(this->m_MediaControl->Run()))
			{
				return true;
			}
		}else
		{
			return true;
		}
	}
	return false;
}

bool CPlayerDlg::IsStopped()
{
	if(m_Graph&&this->m_MediaControl)
	{
		OAFilterState FilterState=State_Stopped;
		if(SUCCEEDED(this->m_MediaControl->GetState(10,&FilterState)))
		{
			if(FilterState==State_Stopped)
			{
				return true;
			}
		}
	}
	return false;
}
bool CPlayerDlg::Stop()
{
	if(m_Graph&&this->m_MediaControl)
	{
		if(!this->IsStopped())
		{
			if(SUCCEEDED(this->m_MediaControl->Stop()))
			{
				return true;
			}
		}else
		{
			return true;
		}
	}
	return false;
}
bool CPlayerDlg::IsPaused()
{
	if(m_Graph&&this->m_MediaControl)
	{
		OAFilterState FilterState=State_Stopped;
		if(SUCCEEDED(this->m_MediaControl->GetState(10,&FilterState)))
		{
			if(FilterState==State_Paused)
			{
				return true;
			}
		}
	}
	return false;
}
bool CPlayerDlg::Pause()
{
	if(m_Graph&&this->m_MediaControl)
	{
		if(!this->IsPaused())
		{
			if(SUCCEEDED(this->m_MediaControl->Pause()))
			{
				return true;
			}
		}else
		{
			return true;
		}
	}
	return false;
}

2、针对IMediaSeeking类的函数封装,主要是针对GetDurationGetCurrentPositionSetPositions函数的封装,主要是用来计算当前S lider控件中托动点的位置用的,这里我只是封装了几个我们播放用的函数,其实IMediaSeeking还有其它的一些函数,也是很好的,大家可以查看下SDK,比如设置播放速率什么的,实现慢放、快放等,这些就靠大家去研究吧

bool CPlayerDlg::GetDuration(double * outDuration)
{
	if (m_Seeking)
	{
		LONGLONG length = 0;
		if (SUCCEEDED(m_Seeking->GetDuration(&length)))
		{
			*outDuration = ((double)length) / 10000000.;
			return true;
		}
	}
	return false;
}
//这里要简单的做一个说明,因为我们在Create()函数中,已经设定了时间格式为TIME_FORMAT_MEDIA_TIME,即是以ns为播放单位的
//也即IMediaSeeking::GetDuration()返回给我们的值是以ns的单位的,我们要化成以秒为单位,要除以的次方
bool CPlayerDlg::GetCurrentPosition(double *outPosition)
{
	if(m_Graph&&this->m_Seeking)
	{
		LONGLONG position=0;
		if(SUCCEEDED(this->m_Seeking->GetCurrentPosition(&position)))
		{
			*outPosition=((double)position) / 10000000.;
			return true;
		}
	}
	return false;
}
//原理与上一个函数类似,不再缀述
bool  CPlayerDlg::SetCurrentPosition(double Position)
{
	if(m_Graph&&this->m_Seeking)
	{
		LONGLONG pos=10000000*Position;//首先转换为正规时间格式
		HRESULT hr=this->m_Seeking->SetPositions(&pos,AM_SEEKING_AbsolutePositioning|AM_SEEKING_SeekToKeyFrame,0,AM_SEEKING_NoPositioning);
	
		if(SUCCEEDED(hr))
		{
			return true;
		}
	}
	return false;
}
//这里主要是将秒为单位的时间,转化为IMediaSeeking可以识别的ns为单位的时间,也就是上面两个函数的反操作

3、针对IVideoWindow类的函数封装,这个就稍微有点难度了,不再是仅仅的对一个函数的封装了,这里是真正的自己实现,我们实现的函数有SetDisplayWindow(用于设置播放窗口),SetFullScreen(用于将视频设置为全屏)、GetFullScreen(获取当前全屏状态),具体实现代码如下:

bool CPlayerDlg::SetFullScreen(bool inEnabled)
{
	if(m_Graph&&this->m_VideoWindow)
	{
		if(SUCCEEDED(this->m_VideoWindow->put_FullScreenMode(inEnabled ? OATRUE : OAFALSE)))
		{
			return true;
		}
	}
	return false;
}
//这里同样是一条函数的封装,我想三目运算符,大家应该还记得吧,这里就不讲了哦
bool CPlayerDlg::GetFullScreen(void)
{
	if (m_VideoWindow)
	{
		long  fullScreenMode = OAFALSE;
		m_VideoWindow->get_FullScreenMode(&fullScreenMode);
		return (fullScreenMode == OATRUE);
	}
	return false;
}
//这个函数难度不大,应该没什么可讲的吧
bool CPlayerDlg::SetDisplayWindow(HWND HWindow)
{
	if(this->m_VideoWindow)
	{
		this->m_VideoWindow->put_Visible(OAFALSE);//首先隐藏窗口

		if(HWindow)
		{
			this->m_VideoWindow->put_Owner(OAHWND(HWindow));

			CRect wndRect;
			::GetClientRect(HWindow,&wndRect);//一定要注意要用WND坐标,而不能桌面坐标
			this->m_VideoWindow->put_Left(0);
			this->m_VideoWindow->put_Top(0);
			this->m_VideoWindow->put_Height((long)wndRect.Height());
			this->m_VideoWindow->put_Width((long)wndRect.Width());  //定义显示窗口位置

			this->m_VideoWindow->put_WindowStyle(WS_CHILD|WS_CLIPCHILDREN|WS_CLIPSIBLINGS);//WS_CLIPSIBLINGS表示重绘时,只重绘这一个窗口,其它窗口不发生重绘。
																							//WS_CLIPCHILDREN表示此窗口不允许被其它窗口覆盖
			this->m_VideoWindow->put_MessageDrain((OAHWND)HWindow); //设置接收鼠标键盘的窗口

			this->m_VideoWindow->put_Visible(OATRUE);

			return true;
		}
	}
	return false;
}
//这个函数应该算是封装里面有点重量级的了,我们传进来所要设为显示窗口的句柄,首先用this->m_VideoWindow->put_Owner(OAHWND(HWindow));
//将视频窗口隐藏,不然当反应速度变慢时,将会产生闪烁,下面就是获取当前GetClientRect()句柄的窗口大小信息,然后设定给m_VideoWindow
//然后用put_MessageDrain()方法,让其接收鼠标键盘信息

4、针对IMediaEventEx类的函数封装,实现对SetNotifyWindow的封装

bool CPlayerDlg::SetNotifyWindow(HWND HWindow)
{
	if(this->m_Event&&HWindow)
	{
		HRESULT hr=this->m_Event->SetNotifyWindow((OAHWND)HWindow,WM_GRAPHNOTIFY,0);
		if(SUCCEEDED(hr))
		{
			return true;
		}
	}
	return false;
}

//定义消息接收的窗口,在其中定义的消息为WM_GRAPHNOTIFY,WM_GRAPHNOTIFY是我们自定义的消息,,

1、在PlayerDlg.h文件中,在顶部添加#define WM_GRAPHNOTIFY  (WM_USER+20)

2、在PlayerDlg.h文件中,并且在头文件中DECLARE_MESSAGE_MAP上面添加afx_msg LRESULT OnGraphNotify(WPARAM inWParam,LPARAM inLParam);位置如图:

3、在PlayerDlg.cpp中如图所示位置在//}}AFX_MSG_MAP下面,添加ON_MESSAGE(WM_GRAPHNOTIFY,OnGraphNotify)

4、最后在PlayerDlg.cpp中增加对响应函数的实现,这里可以先加一个框架,在文章的最后,会贴出具体实现,这里可以先写成

LRESULT CPlayerDlg::OnGraphNotify(WPARAM inWParam, LPARAM inLParam)
{
	return true;
}

5,针对RenderFile()的封装

bool CPlayerDlg::RenderFile(const char * inFile)
{
	if(m_Graph)
	{
		 WCHAR szFilePath[MAX_PATH];
		 MultiByteToWideChar(CP_ACP, 0, inFile, -1, szFilePath, MAX_PATH);//把ASCII编码转换成UNICODE编码

		 if(SUCCEEDED(this->m_Graph->RenderFile(szFilePath,NULL)))
		 {
			 return true;
		 }
	}
	return false;
}

(三)对添加对按钮的响应

一、添加对“打开”按钮的响应

在其响应函数中添加如下代码

void CPlayerDlg::OnBnClickedBtnOpen()
{
	// TODO: 在此添加控件通知处理程序代码
	CString    strFilter = _T("AVI File (*.avi)|*.avi|");
	strFilter +=_T( "MPEG File (*.mpg;*.mpeg)|*.mpg;*.mpeg|");
	strFilter +=_T("Mp3 File (*.mp3)|*.mp3|");
	strFilter +=_T( "Wave File (*.wav)|*.wav|");
	strFilter +=_T( "All Files (*.*)|*.*|");
	CFileDialog dlgOpen(TRUE, NULL, NULL, OFN_PATHMUSTEXIST | OFN_HIDEREADONLY, 
		strFilter, this);
	if (IDOK == dlgOpen.DoModal()) 
	{
		m_SourceFile = dlgOpen.GetPathName();
		this->CreateGraph();
	}
}
//主要是添加一个打开对话框,并在关闭之后,保存打开的文件的路径,然后就是构建Graph;

说明:1、m_SourceFile是定义的一个CStringA 对象,主要是用来保存打开文件的路径,大家自己添加定义和初始化一下吧。

2、CreateGraph()是新封装的一个函数,代码及讲解如下

bool CPlayerDlg::CreateGraph()
{
	this->DestroyGraph();

	if(this->Create())
	{
		this->RenderFile(this->m_SourceFile);

		this->SetDisplayWindow(this->m_VideoWindowPlay.GetSafeHwnd());//设置显示窗口
		this->SetNotifyWindow(this->GetSafeHwnd());//将当前窗口作为接收消息窗口

		this->Pause();
		return true;

	}
	return false;
}


 

//这里主要是先用this->Create()创建Graph中的各个变量及初始化操作,然后this->RenderFile(this->m_SourceFile);来渲染文件,也即采用智能链接模式,自动为我们构建播放链路,然后用SetDisplayWindow和SetNotifyWindow来设置播放窗口和消息接收窗口,最后this->Pause();先将视频暂停,等我们按下“播放”按钮时再播放视频。这里有个函数是我们新添加的,即this->DestroyGraph();,它主要实现的功能是释放变量,实现代码如下:

bool CPlayerDlg::DestroyGraph()
{
	if(this->m_BasicAudio)
	{
		this->m_BasicAudio->Release();
		this->m_BasicAudio=NULL;
	}
	if(this->m_BasicVideo)
	{
		this->m_BasicVideo->Release();
		this->m_BasicVideo=NULL;
	}
	if(this->m_Event)
	{
		this->m_Event->Release();
		this->m_Event=NULL;
	}

	if(this->m_MediaControl)
	{
		this->m_MediaControl->Release();
		this->m_MediaControl=NULL;
	}
	if(this->m_Seeking)
	{
		this->m_Seeking->Release();
		this->m_Seeking=NULL;
	}
	if(this->m_VideoWindow)
	{
		this->m_VideoWindow->put_Visible(OAFALSE);// hide the video window
		this->m_VideoWindow->put_MessageDrain((OAHWND)NULL);//移除处理鼠标和键盘信息接收窗口
		this->m_VideoWindow->put_Owner((OAHWND)NULL);//移除视频窗口的拥有者
		this->m_VideoWindow->Release();
		this->m_VideoWindow=NULL;
	}

	if(this->m_Graph) //一定要最后释放m_Graph
	{
		this->m_Graph->Release();
		this->m_Graph=NULL;
	}
	return true;
}

二、对“播放”按钮的响应

实现代码如下:

void CPlayerDlg::OnBnClickedBtnPlay()
{
	// TODO: 在此添加控件通知处理程序代码
	if(this->m_Graph)
	{
		this->Run();
	}
}

三、对“暂停”按钮的响应

void CPlayerDlg::OnBnClickedBtnPause()
{
	// TODO: 在此添加控件通知处理程序代码
	if(this->m_Graph)
	{
		this->Pause();
	}
}

四,对“停止”按钮的响应

void CPlayerDlg::OnBnClickedBtnStop()
{
	// TODO: 在此添加控件通知处理程序代码
	if(this->m_Graph)
	{
		this->Stop();
		this->SetCurrentPosition(0);
	}
}

(四)添加进度条推进与拖曳响应

1、首先要增加一个定时器,所以在OnBnClickedBtnOpen()、OnBnClickedBtnPause()、OnBnClickedBtnStop()函数中,添加如下代码:

if(this->m_SliderTimer==0)
{
	m_SliderTimer=this->SetTimer(IDC_PROGRESS,100,NULL);
}

//表示如果在点击这个按钮时,如果还没有创建定时器,就创建一个定时器,我们不指定接收消息函数,默认让OnTimer()来处理就可以了,这里有个新变量m_SliderTimer,是UINT 类型的变量,大家自己添加一下定义,记得初始化为0哦,用来保存创建Timer的ID号的。

2、对DLG添加WM_TIMER消息响应

void CPlayerDlg::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	if(nIDEvent==this->m_SliderTimer&&this->m_Graph)
	{
		double pos=0,duration=1;
		this->GetCurrentPosition(&pos);
		this->GetDuration(&duration);

		int newPos = int(pos * 1000 / duration);
		if(this->m_Slider.GetPos()!=newPos)
		{
			this->m_Slider.SetPos(newPos);
		}
	}
	CDialog::OnTimer(nIDEvent);
} 
//这里没什么函数难度,但主要是个思想,也就是根据当前视频的时间来设定滑动标记在Slider的位置

3、实现对鼠标拉动进度条时的消息响应

DLG添加对WM_HSCROLL消息的响应OnHScroll()

代码如下:

void CPlayerDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	if(pScrollBar->GetSafeHwnd()==this->m_Slider.GetSafeHwnd())
	{
		if(this->m_Slider)
		{
			double duration=1;
			double pos=0;
			pos=this->m_Slider.GetPos();
			this->GetDuration(&duration);
			double newPos=duration*pos/1000;
			this->SetCurrentPosition(newPos);
		}
	}else
	{
		CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
	}
}
//这段代码的主要思想,就是根据Slider的位置来设定视频的位置

最后贴出消息响应函数的实现代码:

LRESULT CPlayerDlg::OnGraphNotify(WPARAM inWParam, LPARAM inLParam)
{
	if(this->m_Graph&&this->m_Event)
	{
		long eventCode=0,eventParam1=0,eventParam2=0;
		while(SUCCEEDED(this->m_Event->GetEvent(&eventCode,&eventParam1,&eventParam2,0)))
		{
			m_Event->FreeEventParams(eventCode,eventParam1,eventParam2);

			switch(eventCode)
			{
			case EC_COMPLETE:
				OnBnClickedBtnPause();
				this->SetCurrentPosition(0);
				break;
			case EC_USERABORT:
			case EC_ERRORABORT:
				OnBnClickedBtnStop();
				break;
			default:
				break;
			}
		}
	}
	return 0;
}
//这段代码理解起来应该难度不大,主要就是对消息类型的判断,然后根据不同的消息类型用不同的函数来处理

实现图如下:

 

写上面的内容实在是太累了,暂且写到这吧,下篇将会对播放器进行功能的稍微补充,解码器的安装配置与GraphEdit.exe的使用,而在最后一篇我打算讲解对于手动连接FILTER,以解决有些格式播放有晃动不清的问题,写的不好,还请大家批评指正谢谢大家的观摩。

最后,本文的源码地址是:
http://download.csdn.net/detail/harvic880925/4569951    

不收分,仅供分享技术,转载请标明出处哦!

你可能感兴趣的:(3,视频开发,-------3.1,directShow)