在实现了利用控制台程序播放指定视频文件之后,接下来开始尝试编写一个带有界面的视频播放器,可以选择播放的视频,控制音量等更多的功能。为简单起见,界面的框架用MFC实现。
1、建立工程,生成默认界面
这一步很简单,打开Visual Studio 2010,选择MFC Application,选择基于对话框的工程然后一路next就完成了,没有任何需要更改的地方。之后可以编译运行,生成一个默认的对话框。对工程进行与DirectShow相关的设置,具体方法参考这里。
2、添加播放器内核类
将上文中的DirectShowApi.h+.cpp文件添加到新的工程中,代码如下。
//DirectShowApi.h #pragma once #include <DShow.h> #include <assert.h> HRESULT AddToRot(IUnknown *pUnkGraph, DWORD *pdwRegister); void RemoveFromRot(DWORD pdwRegister); HRESULT AddFilterByCLSID(IGraphBuilder *pGraph, const GUID& clsid, LPCWCHAR wszName, IBaseFilter **ppF); HRESULT GetUnconectedPin(IBaseFilter *pFilter, PIN_DIRECTION PinDir, IPin **ppPin); HRESULT ConnectFilters(IGraphBuilder *pGraph, IPin *pOut, IBaseFilter *pDest); HRESULT ConnectFilters( IGraphBuilder *pGraph, IBaseFilter *pSrc, IBaseFilter *pDest); //DirectShowApi.cpp #include "stdafx.h" #include "DirectShowAPI.h" HRESULT AddToRot(IUnknown *pUnkGraph, DWORD *pdwRegister) { IMoniker * pMoniker = NULL; IRunningObjectTable *pROT = NULL; if (FAILED(GetRunningObjectTable(0, &pROT))) { return E_FAIL; } const size_t STRING_LENGTH = 256; WCHAR wsz[STRING_LENGTH]; StringCchPrintfW( wsz, STRING_LENGTH, L"FilterGraph %08x pid %08x", (DWORD_PTR)pUnkGraph, GetCurrentProcessId() ); HRESULT hr = CreateItemMoniker(L"!", wsz, &pMoniker); if (SUCCEEDED(hr)) { hr = pROT->Register(ROTFLAGS_REGISTRATIONKEEPSALIVE, pUnkGraph, pMoniker, pdwRegister); pMoniker->Release(); } pROT->Release(); return hr; } void RemoveFromRot(DWORD pdwRegister) { IRunningObjectTable *pROT = NULL; if(SUCCEEDED(GetRunningObjectTable(0,&pROT))) { pROT->Revoke(pdwRegister); pROT->Release(); } } HRESULT AddFilterByCLSID( IGraphBuilder *pGraph, const GUID& clsid, LPCWCHAR wszName, IBaseFilter **ppF ) { if (!pGraph || !ppF) return E_POINTER; *ppF = 0; IBaseFilter *pF = 0; HRESULT hr = CoCreateInstance(clsid, 0, CLSCTX_INPROC_SERVER, IID_IBaseFilter, reinterpret_cast<void**>(&pF)); if (SUCCEEDED(hr)) { hr = pGraph->AddFilter(pF, wszName); if (SUCCEEDED(hr)) *ppF = pF; else pF->Release(); } return hr; } HRESULT GetUnconectedPin( IBaseFilter *pFilter, PIN_DIRECTION PinDir, IPin **ppPin ) { *ppPin = 0; IEnumPins *pEnum = 0; IPin *pPin = 0; HRESULT hr = pFilter->EnumPins(&pEnum); if (FAILED(hr)) { return hr; } hr = pEnum->Reset(); while (pEnum->Next(1,&pPin,NULL) == S_OK) { PIN_DIRECTION ThisPinDirection; pPin->QueryDirection(&ThisPinDirection); if (ThisPinDirection == PinDir) { IPin *pTemp = 0; hr = pPin->ConnectedTo(&pTemp); if (SUCCEEDED(hr)) { //当前pin已经连接,无效; pTemp->Release(); } else { pEnum->Release(); *ppPin = pPin; return S_OK; } } pPin->Release(); } pEnum->Release(); return E_FAIL; } HRESULT ConnectFilters( IGraphBuilder *pGraph, IPin *pOut, IBaseFilter *pDest ) { if ((pGraph == NULL)||(pOut == NULL)||(pDest == NULL)) return E_POINTER; #ifdef _DEBUG PIN_DIRECTION PinDir; pOut->QueryDirection(&PinDir); assert(PinDir == PINDIR_OUTPUT); #endif // _DEBUG //得到下级filter的输入pin IPin *pIn = 0; HRESULT hr = GetUnconectedPin(pDest,PINDIR_INPUT,&pIn); if (FAILED(hr)) return hr; hr = pGraph->Connect(pOut,pIn); pIn->Release(); return hr; } HRESULT ConnectFilters( IGraphBuilder *pGraph, IBaseFilter *pSrc, IBaseFilter *pDest) { if ((pGraph == NULL)||(pSrc == NULL)||(pDest == NULL)) return E_POINTER; IPin *pOut = 0; HRESULT hr = GetUnconectedPin(pSrc,PINDIR_OUTPUT,&pOut); if (FAILED(hr)) return hr; hr = ConnectFilters(pGraph,pOut,pDest); pOut->Release(); return hr; }
在工程中添加类CFilterGraph,并在定义类的头文件中添加跟Filter Graph相关的接口成员变量;随后定义创建和删除Filter Graph的方法,代码如下:
定义:
//Filter Graph.h #pragma once #include "DirectShowApi.h" class CFilterGraph { public: CFilterGraph(void); ~CFilterGraph(void); public: //DirectShow相关接口成员 IGraphBuilder *pGraph; //滤波器链表管理器 IMediaControl *pMediaControl; //媒体控制接口,如run、stop、pause IMediaEventEx *pMediaEvent; //媒体事件接口 IBasicVideo *pBasicVideo; //视频基本接口 IBasicAudio *pBasicAudio; //音频基本接口 IVideoWindow *pVideoWindow; //视频窗口接口 DWORD m_dwGraphRegister; public: //CFilterGraph API virtual bool Create(void); //生成滤波器链表管理器 virtual void Release(void); //释放所有接口 private: //内部方法 bool QueryInterfaces(void); };
实现:
//Filter Graph.cpp #include "StdAfx.h" #include "FilterGraph.h" CFilterGraph::CFilterGraph(void) { } CFilterGraph::~CFilterGraph(void) { } bool CFilterGraph::Create( void ) { if (!pGraph) { if (SUCCEEDED(CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&pGraph))) { ::AddToRot(pGraph,&m_dwGraphRegister); return QueryInterfaces(); } } return false; } bool CFilterGraph::QueryInterfaces( void ) { if (pGraph) { HRESULT hr = NOERROR; hr |= pGraph->QueryInterface(IID_IMediaControl, (void **)&pMediaControl); hr |= pGraph->QueryInterface(IID_IMediaEventEx, (void **)&pMediaEvent); hr |= pGraph->QueryInterface(IID_IBasicVideo, (void **)&pBasicVideo); hr |= pGraph->QueryInterface(IID_IBasicAudio, (void **)&pBasicAudio); hr |= pGraph->QueryInterface(IID_IVideoWindow, (void **)&pVideoWindow); return SUCCEEDED(hr); } return false; } void CFilterGraph::Release( void ) { if (pMediaControl) { pMediaControl->Release(); pMediaControl = NULL; } if (pMediaEvent) { pMediaEvent->Release(); pMediaEvent = NULL; } if (pBasicVideo) { pBasicVideo->Release(); pBasicVideo = NULL; } if (pBasicAudio) { pBasicAudio->Release(); pBasicAudio = NULL; } if (pVideoWindow) { pVideoWindow->put_Visible(OAFALSE); pVideoWindow->put_MessageDrain((OAHWND)NULL); pVideoWindow->put_Owner(OAHWND(0)); pVideoWindow->Release(); pVideoWindow = NULL; } ::RemoveFromRot(m_dwGraphRegister); if (pGraph) { pGraph->Release(); pGraph = NULL; } }
现在这个类还不完整,稍后将会把这个类补全。
3、简要设计对话框的界面
在MFC默认的对话框界面中,添加一个按键用于选择视频文件,以及一个标签用于显示选定的文件名,如下图所示。
为按键添加相应函数:
void CLavPlayerDlg::OnBnClickedButtonSelectfile() { // TODO: Add your control notification handler code here CString strFormatFilter = _T("AVI file (*.avi)|*.avi|"); strFormatFilter += _T("MPEG file (*.mpg;*.mpeg;*.mp4)|*.mpg;*.mpeg;*.mp4|"); strFormatFilter += _T("HD file (*.mkv;*.ts)|*.mkv;*.ts|"); strFormatFilter += _T("Audio file (*.mp3;*.aac)|*.mp3;*.aac|"); strFormatFilter += _T("All file (*.*)|*.*|"); CFileDialog dlg(TRUE,NULL,NULL,OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,strFormatFilter,this); if (dlg.DoModal() == IDOK) { m_VideoFilePath = dlg.GetPathName(); m_VideoFileName = GetFileTitleFromFileName(m_VideoFilePath,1); CreateGraph(); } } CString CLavPlayerDlg::GetFileTitleFromFileName( CString FileName, bool Ext ) { int Where; Where = FileName.ReverseFind('\\'); if (Where == -1) Where = FileName.ReverseFind('/'); CString FileTitle = FileName.Right(FileName.GetLength() - 1 - Where); if (!Ext) { int Which = FileTitle.ReverseFind('.'); if (Which != -1) FileTitle = FileTitle.Left(Which); } return FileTitle; }
其中的m_VideoFilePath和m_VideoFileName是定义在dlg类中的两个cstring类的成员,用于接收选定视频的文件路径和文件名。CreateGraph()用于创建完整的filter graph。
void CLavPlayerDlg::CreateGraph() { DestroyGraph(); m_pFilterGraph = new CFilterGraph(); if (m_pFilterGraph->Create()) { if (m_pFilterGraph->RenderFile(m_VideoFilePath)) { AfxMessageBox(_T("无法播放文件,因为文件已损坏或缺少解码器!")); return; } // m_pFilterGraph->SetDisplayWindow(m_videoWindow.GetSafeHwnd()); m_pFilterGraph->SetNotifyWindow(this->GetSafeHwnd()); m_pFilterGraph->Pause(); } } void CLavPlayerDlg::DestroyGraph() { if (m_pFilterGraph != NULL) { m_pFilterGraph->Stop(); m_pFilterGraph->SetNotifyWindow(NULL); delete m_pFilterGraph; m_pFilterGraph = NULL; } }
播放器核心类中添加了多个函数。如Stop、Pause、SetNotifyWindow、SetDisplayWindow、RenderFile等,分别用于停止视频播放、暂停视频播放、设置通知窗口、设置显示窗口、解析播放视频文件等。下面首先考虑实现RenderFile方法。
首先需要添加头文件定义lavfilter组件的clsid,并在播放器核心类中include这个头文件:
//CLSID.h #pragma once #include <InitGuid.h> // {B98D13E7-55DB-4385-A33D-09FD1BA26338} static const GUID CLSID_LavSplitter_Source = { 0xB98D13E7, 0x55DB, 0x4385, { 0xA3, 0x3D, 0x09, 0xFD, 0x1B, 0xA2, 0x63, 0x38 } }; //{EE30215D-164F-4A92-A4EB-9D4C13390F9F} static const GUID CLSID_LavVideoDecoder = { 0xEE30215D, 0x164F, 0x4A92, { 0xA4, 0xEB, 0x9D, 0x4C, 0x13, 0x39, 0x0F, 0x9F } }; //{E8E73B6B-4CB3-44A4-BE99-4F7BCB96E491} static const GUID CLSID_LavAudioDecoder = { 0xE8E73B6B, 0x4CB3, 0x44A4, { 0xBE, 0x99, 0x4F, 0x7B, 0xCB, 0x96, 0xE4, 0x91 } };
同时参考上篇文章中的控制台应用,在RenderFile函数中添加lavfilter的组件:
bool CFilterGraph::RenderFile( CString fileName ) { HRESULT hr = NOERROR; LPTSTR fileToPlay = fileName.GetBuffer(); hr |= ::AddFilterByCLSID(pGraph,CLSID_LavSplitter_Source,L"Lav Splitter Source",&m_pLavSplitterSource); hr |= m_pLavSplitterSource->QueryInterface(IID_IFileSourceFilter,(void **)&m_pFileSourceFilter); hr |= m_pFileSourceFilter->Load(fileToPlay,NULL); fileName.ReleaseBuffer(); hr |= AddFilterByCLSID(pGraph,CLSID_LavVideoDecoder,L"Lav Video Decoder",&m_pLavVideoDecoder); hr |= ConnectFilters(pGraph,m_pLavSplitterSource,m_pLavVideoDecoder); hr |= AddFilterByCLSID(pGraph,CLSID_LavAudioDecoder,L"Lav Audio Decoder",&m_pLavAudioDecoder); hr |= ConnectFilters(pGraph,m_pLavSplitterSource,m_pLavAudioDecoder); hr |= AddFilterByCLSID(pGraph,CLSID_VideoMixingRenderer9,L"Video Mixing Renderer-9",&m_pVideoRenderer); hr |= ConnectFilters(pGraph,m_pLavVideoDecoder,m_pVideoRenderer); hr |= AddFilterByCLSID(pGraph,CLSID_AudioRender,L"Audio render",&m_pAudioRender); hr |= ConnectFilters(pGraph,m_pLavAudioDecoder,m_pAudioRender); return SUCCEEDED(hr); }
同时在界面上新增加一个按钮,命名为“播放”,初始显示设置为FALSE,在其响应函数中调用播放内核类的run方法,使Filter Graph Manager开始运行:
void CLavPlayerDlg::OnBnClickedBtnPlay() { // TODO: Add your control notification handler code here m_pFilterGraph->Run(); } bool CFilterGraph::Run( void ) { if (pGraph && pMediaControl) { if (!IsRunning()) { if (SUCCEEDED(pMediaControl->Run())) { return true; } } else { return true; } } return false; } bool CFilterGraph::IsRunning( void ) { if (pGraph && pMediaControl) { OAFilterState state = State_Stopped; if (SUCCEEDED(pMediaControl->GetState(10, &state))) { return state == State_Running; } } return false; }
除此之外还需要在CreateGraph中添加Label显示文件名,以及显示播放按钮的功能,完成后效果如下:
void CLavPlayerDlg::CreateGraph() { DestroyGraph(); m_pFilterGraph = new CFilterGraph(); if (m_pFilterGraph->Create()) { if (m_pFilterGraph->RenderFile(m_VideoFilePath) == false) { AfxMessageBox(_T("无法播放文件,因为文件已损坏或缺少解码器!")); return; } m_pFilterGraph->SetNotifyWindow(this->GetSafeHwnd()); m_pFilterGraph->Pause(); GetDlgItem(IDC_VIDEOFILE)->SetWindowText(m_VideoFileName); GetDlgItem(IDC_BTN_PLAY)->ShowWindow(TRUE); } }
编译运行,初始界面如:
选择文件后,出现“播放”按钮:
播放之后的界面:
虽然视频的播放功能已经实现但是应用却还是十分的不完善(比如没有播放控制功能,没有音量调节等),还有很多漏洞,以后会在学习的过程中逐步改进。
本文DEMO下载请点击这里