/*
* blackboy [email protected]
* QQ群: 135202158
* 转载请注明作者及出处
*/
参考MSDN官方的页面:http://msdn.microsoft.com/en-us/library/ms703190(v=vs.85)
本文详细演示了如何使用Media Foundation中的Media Session对象来播放媒体文件。也就是不自己编写/自定义任何的Media Foundation组件,一切都是用现成的,以及让Media Foundation“自动完成”的(如Topology的解析)。Media Foundation的API会根据文件的路径或URL智能创建合适的media source组件,并会智能地在media source和音视频渲染器(renderer)之间添加合适的解码器等等。Topology中的数据流等任务由Media Session来处理。
这是最简单的开发任务。然而,如果要实现使用自定义的meida source或media transform组件这样的任务,可能不能使用Media Session。
在阅读本主题之前,你需要熟悉以下MF概念:
注意:此主题不描述如何播放被DRM保护的文件。关于MMF中DRM的相关信息,见 How to Play Protected Media Files。
其实不太了解以上概念也没关系,通过这个小例子的动手实践,我们会对一些基本概念有个基本了解。
以下对象用来和Media Session播放多媒体文件:
大概了解一下概念,我们可以来进行实践了。我们主要将完成以下任务:
我使用visual studio 2012创建了一个基于对话框的MFC项目。含有可缩放的边框、 最小化框。
再创建一个菜单,把对话框的菜单属性设为此菜单。
添加全局的播放核心类对象
Core* g_pCore = NULL;
在stdafx.h中添加要用到的头文件和类模板。头文件后面的注释说明了为什么需要它
#include <mfapi.h> // MFStartup, mfplat.lib #include <mfidl.h> // MFCreateMediaSession, mf.lib #include <evr.h> // IMFVideoDisplayControl, strmiids.lib #include <shlwapi.h> // QITABENT, shlwapi.lib #include <mferror.h> // MF_E_ALREADY_INITIALIZED template <class T> void SafeRelease(T **ppT) { if (*ppT) { (*ppT)->Release(); *ppT = NULL; } } #include "Core.h" // Core类头文件
mfplat.lib; mf.lib; mfuuid.lib; strmiids.lib; shlwapi.lib
首先定义播放事件标识和播放状态的枚举
const UINT WM_APP_PLAYER_EVENT = WM_APP + 1; enum PlayerState { Closed = 0, // No session. Ready, // Session was created, ready to open a file. OpenPending, // Session is opening a file. Started, // Session is playing a file. Paused, // Session is paused. Stopped, // Session is stopped (ready to play). Closing // Application has closed the session, // but is waiting for MESessionClosed. };
此类被设计为单例模式(singleton),为了处理事件方便,它继承自IMFAsyncCallback接口,此接口又继承自IUnknown接口。因此我们要为Core类实现这些接口的所有必需方法。
类声明:
class Core : public IMFAsyncCallback { protected: Core(HWND hVideo); virtual ~Core(void); public: static HRESULT CreateInstance(HWND hVideo, Core** ppCore); // IUnknown方法 STDMETHODIMP QueryInterface(REFIID iid, void** ppv); STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); // IMFAsyncCallback方法 STDMETHODIMP GetParameters(DWORD*, DWORD*) { return E_NOTIMPL;} STDMETHODIMP Invoke(IMFAsyncResult* pAsyncResult); HRESULT Initialize(); HRESULT OpenFile(PCWSTR sURL); HRESULT HandleEvent(UINT_PTR pEvent); PlayerState GetState() const { return m_state; } BOOL HasVideo() const { return (m_pVideoControl != NULL); } HRESULT StartPlay(); HRESULT Play(); HRESULT Pause(); HRESULT Stop(); HRESULT Shutdown(); HRESULT Repaint(); HRESULT ResizeVideo(WORD width, WORD height); HRESULT OnTopologyStatus(IMFMediaEvent*); HRESULT OnPresentationEnded(IMFMediaEvent*); private: long m_nRefCount; PlayerState m_state; IMFMediaSession* m_pMediaSession; IMFMediaSource* m_pMediaSource; IMFVideoDisplayControl* m_pVideoControl; HWND m_hwndVideo; HANDLE m_hCloseEvent; };
Core::Core(HWND hVideo) : m_pMediaSession(NULL), m_pMediaSource(NULL), m_pVideoControl(NULL), m_hwndVideo(hVideo), m_hCloseEvent(NULL), m_state(Closed), m_nRefCount(1) { } Core::~Core(void) { if(m_pMediaSession) { Shutdown(); } } HRESULT Core::QueryInterface(REFIID riid, void** ppv) { static const QITAB qit[] = { QITABENT(Core, IMFAsyncCallback), { 0 } }; return QISearch(this, qit, riid, ppv); } ULONG Core::AddRef() { return InterlockedIncrement(&m_nRefCount); } ULONG Core::Release() { ULONG uCount = InterlockedDecrement(&m_nRefCount); if(uCount == 0) delete this; return uCount; } HRESULT Core::CreateInstance(HWND hVideo, Core** ppCore) { if(hVideo == NULL) return E_UNEXPECTED; if(ppCore == NULL) return E_POINTER; Core* pCore = new Core(hVideo); if(pCore == NULL) return E_OUTOFMEMORY; HRESULT hr = pCore->Initialize(); if(SUCCEEDED(hr)) { *ppCore = pCore; (*ppCore)->AddRef(); } SafeRelease(&pCore); return hr; } HRESULT Core::Initialize() { if(m_hCloseEvent) return MF_E_ALREADY_INITIALIZED; HRESULT hr = MFStartup(MF_VERSION); if(FAILED(hr)) return hr; m_hCloseEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if(m_hCloseEvent == NULL) hr = HRESULT_FROM_WIN32(GetLastError()); return hr; }
这是一个核心的部分,代码较多,为了简单,我把所有代码都写到一个函数里去了,主要是为了理解方便。
主要完成了以下工作:
HRESULT Core::OpenFile(PCWSTR sURL) { assert(m_state == Closed || m_state == Stopped); // 创建Media Session HRESULT hr = MFCreateMediaSession(NULL, &m_pMediaSession); if(FAILED(hr)) return hr; m_state = Ready; // 开始从Media Session取得事件 hr = m_pMediaSession->BeginGetEvent((IMFAsyncCallback*)this, NULL); if(FAILED(hr)) return hr; // 创建Media Source IMFSourceResolver* pSourceResolver = NULL; IUnknown* pUnknown = NULL; IMFTopology* pTopology = NULL; IMFPresentationDescriptor* pPD = NULL; MF_OBJECT_TYPE objType = MF_OBJECT_INVALID; SafeRelease(&m_pMediaSource); hr = MFCreateSourceResolver(&pSourceResolver); if(FAILED(hr)) goto over; // 为简单,弄成同步方法 hr = pSourceResolver->CreateObjectFromURL( sURL, MF_RESOLUTION_MEDIASOURCE, NULL, &objType, &pUnknown); if(FAILED(hr)) goto over; hr = pUnknown->QueryInterface(IID_PPV_ARGS(&m_pMediaSource)); if(FAILED(hr)) goto over; // 创建Topology assert(m_pMediaSession != NULL); assert(m_pMediaSource != NULL); DWORD cStreams = 0; hr = MFCreateTopology(&pTopology); if(FAILED(hr)) goto over; // 创建PresentationDescriptor hr = m_pMediaSource->CreatePresentationDescriptor(&pPD); if(FAILED(hr)) goto over; // 获取source中的stream数目 hr = pPD->GetStreamDescriptorCount(&cStreams); if(FAILED(hr)) goto over; assert(pTopology != NULL); // 为每个stream创建topology节点,并将其加入到topology for(DWORD i = 0; i<cStreams; i++ ) { IMFStreamDescriptor* pSD = NULL; IMFTopologyNode* pSourceNode = NULL; IMFTopologyNode* pOutputNode = NULL; BOOL bSelected = FALSE; hr= pPD->GetStreamDescriptorByIndex(i, &bSelected, &pSD); if(FAILED(hr)) goto over2; if(bSelected) { // source node hr = MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE, &pSourceNode); if(FAILED(hr)) goto over2; hr = pSourceNode->SetUnknown(MF_TOPONODE_SOURCE, m_pMediaSource); if(FAILED(hr)) goto over2; hr = pSourceNode->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR, pPD); if(FAILED(hr)) goto over2; hr = pSourceNode->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR, pSD); if(FAILED(hr)) goto over2; // output node IMFMediaTypeHandler* pHandler = NULL; IMFActivate* pRendererActivate = NULL; GUID guidMajorType = GUID_NULL; DWORD id = 0; pSD->GetStreamIdentifier(&id); // 忽略错误 hr = pSD->GetMediaTypeHandler(&pHandler); if(FAILED(hr)) goto over3; hr = pHandler->GetMajorType(&guidMajorType); if(FAILED(hr)) goto over3; hr = MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &pOutputNode); if(FAILED(hr)) goto over3; if(MFMediaType_Audio == guidMajorType) hr = MFCreateAudioRendererActivate(&pRendererActivate); else if(MFMediaType_Video == guidMajorType) hr = MFCreateVideoRendererActivate(m_hwndVideo, &pRendererActivate); else hr = E_FAIL; if(FAILED(hr)) goto over3; hr = pOutputNode->SetObject(pRendererActivate); if(FAILED(hr)) goto over3; // 把source节点和输出节点添加到topology,并连接它们 hr = pTopology->AddNode(pSourceNode); if(FAILED(hr)) goto over3; hr = pTopology->AddNode(pOutputNode); if(FAILED(hr)) goto over3; hr = pSourceNode->ConnectOutput(0, pOutputNode, 0); over3: SafeRelease(&pRendererActivate); SafeRelease(&pHandler); goto over2; } over2: SafeRelease(&pSD); SafeRelease(&pSourceNode); SafeRelease(&pOutputNode); } hr = m_pMediaSession->SetTopology(0, pTopology); if(FAILED(hr)) goto over; m_state = OpenPending; over: if(FAILED(hr)) m_state = Closed; SafeRelease(&pPD); SafeRelease(&pTopology); SafeRelease(&pSourceResolver); SafeRelease(&pUnknown); return hr; }
主对话框需要提供一个文件/打开菜单,用来打开文件,它的响应如下。由于IMFSourceResolver::CreateObjectFromURL方法只支持LPCWSTR类型的文件路径字符串参数,所以当我们以多字节配置build程序时,需要添加代码,把打开文件对话框取得的多字节文件路径字符串转换成宽字符串。
void CBlackPlayerDlg::OnOpen() { HRESULT hr = S_OK; TCHAR path[MAX_PATH]; path[0] = _T('\0'); OPENFILENAME ofn; ::ZeroMemory(&ofn, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = this->GetSafeHwnd(); ofn.lpstrFilter = _T("Media Files\0*.asf;*.avi;*.mp3;") _T("*.mp4;*.wav;*.wma;*.wmv\0All files\0*.*\0"); ofn.lpstrFile = path; ofn.nMaxFile = MAX_PATH; ofn.Flags = OFN_FILEMUSTEXIST; ofn.hInstance = AfxGetInstanceHandle(); if(::GetOpenFileName(&ofn)) { #ifdef UNICODE hr = g_pCore->OpenFile(ofn.lpstrFile); #else size_t cLen = 0, cChars = 0; cLen = _tcslen(ofn.lpstrFile); WCHAR wstr[MAX_PATH*2] = {L'\0'}; ::MultiByteToWideChar(CP_ACP, 0, ofn.lpstrFile, -1, (LPWSTR)wstr, MAX_PATH); hr = g_pCore->OpenFile(wstr); #endif if(SUCCEEDED(hr)) { UpdateUI(g_pCore->GetState()); } else { AfxMessageBox(_T("无法打开文件!")); } } }
第3步代码的开头我们已经使用BeginGetEvent方法开始获取Media Session的事件,这是一个异步方法,当下一个事件发生时,media session会调用IMFAsyncCallback::Invoke方法。注意此方法是在worker线程调用的,不是主程序所在线程,所以这方法必须线程安全。
HRESULT Core::Invoke(IMFAsyncResult* pResult) { MediaEventType meType = MEUnknown; IMFMediaEvent* pEvent = NULL; // 从事件队列中获取事件 HRESULT hr = m_pMediaSession->EndGetEvent(pResult, &pEvent); if(FAILED(hr)) goto over; // 取得事件的类型 hr = pEvent->GetType(&meType); if(FAILED(hr)) goto over; if(meType == MESessionClosed) { ::SetEvent(m_hCloseEvent); } else { hr = m_pMediaSession->BeginGetEvent(this, NULL); if(FAILED(hr)) goto over; } if(m_state != Closing) { pEvent->AddRef(); ::PostMessage(m_hwndVideo, WM_APP_PLAYER_EVENT, (WPARAM)pEvent, (LPARAM)0); } over: SafeRelease(&pEvent); return S_OK; }Invoke方法中,我们先用EndGetEvent取得事件,接着用PostMessage将此事件发送给主程序窗口,其实主窗口还是把它送回给了Core的HandleEvent方法来进行实际处理。还要再次调用BeginGetEvent方法来异步获取下一事件。所以主对话框类要添加WM_APP_PLAYER_EVENT事件的处理程序:
afx_msg LRESULT CBlackPlayerDlg::OnPlayerEvent(WPARAM wParam, LPARAM lParam) { HRESULT hr = S_OK; hr = g_pCore->HandleEvent(wParam); if(FAILED(hr)) AfxMessageBox(_T("事件处理发生错误!")); UpdateUI(g_pCore->GetState()); return 0; }
以下是Core类的HandleEvent,还可以根据需要添加更多的事件处理:
HRESULT Core::HandleEvent(UINT_PTR pEventPtr) { HRESULT hrStatus = S_OK; HRESULT hr = E_FAIL; IMFMediaEvent* pEvent = NULL; IUnknown* pUnk = (IUnknown*)pEventPtr; if(pUnk == NULL) return E_POINTER; hr = pUnk->QueryInterface(IID_PPV_ARGS(&pEvent)); if(FAILED(hr)) goto over; MediaEventType meType = MEUnknown; hr = pEvent->GetType(&meType); if(FAILED(hr)) goto over; hr = pEvent->GetStatus(&hrStatus); if(FAILED(hr)) goto over; if(FAILED(hrStatus)) { hr = hrStatus; goto over; } switch(meType) { case MESessionTopologyStatus: hr = OnTopologyStatus(pEvent); break; case MEEndOfPresentation: hr = OnPresentationEnded(pEvent); break; default: break; } over: SafeRelease(&pUnk); SafeRelease(&pEvent); return hr; }
HRESULT Core::OnTopologyStatus(IMFMediaEvent* pEvent) { MF_TOPOSTATUS status; HRESULT hr = pEvent->GetUINT32(MF_EVENT_TOPOLOGY_STATUS, (UINT32*)&status); if(SUCCEEDED(hr) && (status == MF_TOPOSTATUS_READY)) { SafeRelease(&m_pVideoControl); // 如果source没有视频stream,此方法将失败 (void)MFGetService(m_pMediaSession, MR_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_pVideoControl)); hr = StartPlay(); } return hr; } HRESULT Core::OnPresentationEnded(IMFMediaEvent* pEvent) { m_state = Stopped; return S_OK; }
构建好完整的topology、设置好事件处理之后,可以真正地开始播放了,以下是播放的一些相关代码:
// 从当前位置开始播放 HRESULT Core::StartPlay() { PROPVARIANT varStart; PropVariantInit(&varStart); varStart.vt = VT_EMPTY; // start是异步方法 HRESULT hr = m_pMediaSession->Start(&GUID_NULL, &varStart); if(SUCCEEDED(hr)) m_state = Started; PropVariantClear(&varStart); return hr; } HRESULT Core::Play() { if(m_state != Paused && m_state != Stopped) return INET_E_INVALID_REQUEST; if(m_pMediaSession == NULL || m_pMediaSource == NULL) return E_UNEXPECTED; return StartPlay(); } HRESULT Core::Pause() { if(m_state != Started) return INET_E_INVALID_REQUEST; if(m_pMediaSession == NULL || m_pMediaSource == NULL) return E_UNEXPECTED; HRESULT hr = m_pMediaSession->Pause(); if(SUCCEEDED(hr)) m_state = Paused; return hr; } HRESULT Core::Stop() { if(m_state != Started && m_state != Paused) return INET_E_INVALID_REQUEST; if(m_pMediaSession == NULL) return E_UNEXPECTED; HRESULT hr = m_pMediaSession->Stop(); if(SUCCEEDED(hr)) m_state = Stopped; return hr; }
视频render(EVR)在我们提供的视频窗口绘制视频图象,这发生在一个工作线程,一般不需要管它。但如果播放暂停或停止时,当视频窗口接收到WM_PAINT消息时,我们必须通知EVR,方法是调用IMFVideoDisplayControl::RepaintVideo方法:
HRESULT Core::Repaint() { if(m_pVideoControl) return m_pVideoControl->RepaintVideo(); else return S_OK; }同时,我们还需要修改主对话框的OnPaint事件处理函数:
void CBlackPlayerDlg::OnPaint() { if (IsIconic()) { // 略。。。。 } else { if(g_pCore && g_pCore->HasVideo()) g_pCore->Repaint(); CDialogEx::OnPaint(); } }
HRESULT Core::ResizeVideo(WORD width, WORD height) { if(m_pVideoControl) { RECT rc = {0, 0, width, height}; return m_pVideoControl->SetVideoPosition(NULL, &rc); } else return S_OK; }类似与重绘,我们也要相应修改主对话框类,为其添加WM_SIZE的消息处理程序:
void CBlackPlayerDlg::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); if(g_pCore != NULL && g_pCore->HasVideo()) g_pCore->ResizeVideo(cx, cy); }
播放完成后我们需要做一些清理工作,比如析构函数中的ShutDown方法:
HRESULT Core::Shutdown() { HRESULT hr = S_OK; SafeRelease(&m_pVideoControl); if(m_pMediaSession) { DWORD dwRet = 0; m_state = Closing; hr = m_pMediaSession->Close(); if(FAILED(hr)) goto over; dwRet = WaitForSingleObject(m_hCloseEvent, 3000); } // 以下shutdown都是同步方法,无事件 if(m_pMediaSource) m_pMediaSource->Shutdown(); if(m_pMediaSession) m_pMediaSession->Shutdown(); SafeRelease(&m_pMediaSource); SafeRelease(&m_pMediaSession); m_state = Closed; MFShutdown(); if(m_hCloseEvent) { ::CloseHandle(m_hCloseEvent); m_hCloseEvent = NULL; } over: return hr; }
CBlackPlayerDlg::~CBlackPlayerDlg() { if(g_pCore != NULL) { g_pCore->Shutdown(); g_pCore->Release(); } }
如果要用键盘的空格键控制视频的暂停/重新播放,主对话框类需要重载PreTranslateMessage函数:
// 必须直接重载此函数,直接处理WM_CHAR或WM_KEYUP消息不行 BOOL CBlackPlayerDlg::PreTranslateMessage(MSG* pMsg) { // TODO: 在此添加专用代码和/或调用基类 if(g_pCore != NULL && pMsg->message == WM_KEYUP) { if(pMsg->wParam == VK_SPACE) { if(g_pCore->GetState() == Started) g_pCore->Pause(); else if(g_pCore->GetState() == Paused) g_pCore->Play(); } return TRUE; } return CDialogEx::PreTranslateMessage(pMsg); }
void CBlackPlayerDlg::OnPause() { if(g_pCore != NULL) { if(g_pCore->GetState() == Started) g_pCore->Pause(); } } void CBlackPlayerDlg::OnStop() { if(g_pCore != NULL) { if(g_pCore->GetState() == Started || g_pCore->GetState() == Paused) g_pCore->Stop(); } }