基於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<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()函數中,添加初始化代碼:
- <span style="font-size:14px;">HRESULT hr=CoInitialize(NULL);
- if(FAILED(hr))
- {
- printf("ERROR-Could not initialize COM libray");
- return -1;
- }
- </span>
位置如圖:
然後添加在CPlayerApp類中添加ExitInstance()函數,在其中添加::CoUninitialize();以解除使用
第三步:實戰
(一)變量定義、初始化及實例化
1、變量定義:
- <span style="font-size:14px;"> 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; //主要用來媒體定位</span>
以上只是對各變量功能作了下簡單的講解,如果想具體瞭解,可以查看SDK或MSDN
2、初始化
CPlayerDlg::OnInitDialog()在添加初始化信息:
- <span style="font-size:14px;"> 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);</span>
3、實例化
在CPlayerDlg類中,添加一個函數Create();專門用來實例化各個變量
- <span style="font-size:14px;">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;
- }</span>
(二)針對更接口函數的封裝
1、針對IMediaControl類的函數封裝,封裝Run(),Stop,Pause()函數,及IsRun(),IsStop(),IsPause()函數的實現,代碼簡單,不再詳述
- <span style="font-size:14px;">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;
- }</span>
2、針對IMediaSeeking類的函數封裝,主要是針對GetDuration、GetCurrentPosition、SetPositions函數的封裝,主要是用來計算當前S lider控件中託動點的位置用的,這裏我只是封裝了幾個我們播放用的函數,其實IMediaSeeking還有其它的一些函數,也是很好的,大家可以查看下SDK,比如設置播放速率什麼的,實現慢放、快放等,這些就靠大家去研究吧
- <span style="font-size:14px;">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爲單位的時間,也就是上面兩個函數的反操作</span>
3、針對IVideoWindow類的函數封裝,這個就稍微有點難度了,不再是僅僅的對一個函數的封裝了,這裏是真正的自己實現,我們實現的函數有SetDisplayWindow(用於設置播放窗口),SetFullScreen(用於將視頻設置爲全屏)、GetFullScreen(獲取當前全屏狀態),具體實現代碼如下:
- <span style="font-size:14px;">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()方法,讓其接收鼠標鍵盤信息</span>
4、針對IMediaEventEx類的函數封裝,實現對SetNotifyWindow的封裝
- <span style="font-size:14px;">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;
- }</span>
//定義消息接收的窗口,在其中定義的消息爲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中增加對響應函數的實現,這裏可以先加一個框架,在文章的最後,會貼出具體實現,這裏可以先寫成
- <span style="font-size:14px;">LRESULT CPlayerDlg::OnGraphNotify(WPARAM inWParam, LPARAM inLParam)
- {
- return true;
- }</span>
5,針對RenderFile()的封裝
- <span style="font-size:14px;">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;
- }</span>
(三)對添加對按鈕的響應
一、添加對“打開”按鈕的響應
在其響應函數中添加如下代碼
- 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,以解決有些格式播放有晃動不清的問題,寫的不好,還請大家批評指正謝謝大家的觀摩。