总前言:我打算写一个能实现全能播放的播放器,功能比较简单,也算是抛砖引玉吧,因为内容较多,所以打算写三篇,这是开篇,欢迎大家吐槽
简易播放器的实现
本文的编写环境: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<streams.h>
(二)界面设置
界面如下:
说明:
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组件
因为DirectShow是COM组件,所以我们在使用前要先对其初始化,用完之后,也要手动解除
在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; //主要用来媒体定位
以上只是对各变量功能作了下简单的讲解,如果想具体了解,可以查看SDK或MSDN
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类的函数封装,主要是针对GetDuration、GetCurrentPosition、SetPositions函数的封装,主要是用来计算当前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
不收分,仅供分享技术,转载请标明出处哦!