音频播放 via Media Foundation I
音频播放 via Media Foundation I
这里所说的播放是指通过读取声音文件数据然后经过解码输出到扬声器进行播放的过程。
Windows 上有如下几种常见的实现方式:
- Waveform API
- FFmpeg
- DirectShow
- Media Foundation I
- Media Foundation II
Media Foundation 简介
Media Foundation (简称 MF)是微软在 Windows Vista上 推出的新一代多媒体应用库,目的是提供 Windows 平台一个统一的多媒体影音解决方案,开发者可以通过 MF 播放视频或声音文件、进行多媒体文件格式转码,或者将一连串图片编码为视频等等。
MF 是 DirectShow 为主的旧式多媒体应用程序接口的替代者与继承者,在微软的计划下将逐步汰换 DirectShow 技术。MF 要求 Windows Vista 或更高版本,不支持较早期的 Windows 版本,特别是 Windows XP。
MF 长于高质量的音频和视频播放,高清内容(如 HDTV,高清电视)和数字版权管理(DRM)访问控制。MF 在不同的 Windows 版本上能力不同,如 Windows 7 上就添加了 h.264 编码支持。Windows 8 上则提供数种更高质量的设置。
MF 提供了两种编程模型,第一种是以 Media Session 为主的 Media pipeline 模型,但是该模型太过复杂,且曝露过多底层细节,故微软于 Windows 7 上推出第二种编程模型,内含 SourceReader、Transcode API 、SinkWriter 及 MFPlay 等高度封装模块,大大简化了 MF 的使用难度。
# 本文使用了第一种(复杂的)编程模型。
Media Foundation 播放音频
播放流程图
播放代码
以下是整个播放过程的概要代码,略去错误处理和一些函数的具体实现:
hr = MFCreateMediaSession(NULL, &m_pSession);
hr = m_pSession->BeginGetEvent((IMFAsyncCallback*)this, NULL);
hr = MFCreateSourceResolver(&pSourceResolver);
hr = pSourceResolver->CreateObjectFromURL( sURL,
// indicate that we want a source object, and pass in optional source search parameters
MF_RESOLUTION_MEDIASOURCE | MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE,
NULL, &objectType, &pSource );
hr = MFCreateTopology(&m_pTopology);
for (DWORD i = 0; i < nSourceStreams; i++) {
hr = pPresDescriptor->GetStreamDescriptorByIndex(i, &streamSelected, &pStreamDescriptor);
hr = CreateSourceStreamNode(pPresDescriptor, pStreamDescriptor, pSourceNode);
hr = CreateOutputNode(pStreamDescriptor, m_videoHwnd, pOutputNode);
hr = m_pTopology->AddNode(pSourceNode);
hr = m_pTopology->AddNode(pOutputNode);
hr = pSourceNode->ConnectOutput(0, pOutputNode, 0);
}
hr = m_pSession->SetTopology(0, pTopology);
hr = m_pSession->Start(&GUID_NULL, &varStart);
// playing...
hr = m_pSession->Stop();
hr = m_pSession->Close();
m_pSession->Shutdown();
CPlayer::OpenURL 函数
创建 Media Session 和 Topology(相当于 DShow 的 Filter Graph)。
HRESULT CPlayer::OpenURL(PCWSTR sURL)
{
CComPtr<IMFTopology> pTopology = NULL;
HRESULT hr = S_OK;
MFUtil::AutoLock lock(m_critSec);
if (m_pSession)
Stop();
hr = CreateSession();
GOTO_IF_FAILED(hr);
hr = m_topoBuilder.RenderURL(sURL, m_hwndVideo);
GOTO_IF_FAILED(hr);
pTopology = m_topoBuilder.GetTopology();
hr = m_pSession->SetTopology(0, pTopology);
GOTO_IF_FAILED(hr);
// If a brand new topology was just created, set the player state to "open pending"
// - not playing yet, but ready to begin.
if (m_state == PlayerState_Ready)
m_state = PlayerState_OpenPending;
return S_OK;
RESOURCE_FREE:
if (FAILED(hr))
m_state = PlayerState_Closed;
return hr;
}
CPlayer::CreateSession 函数
MF 可以说完全是异步驱动的,下面通过 BeginGetEvent 注册了回调对象(自己),MF 的消息将通过 IMFAsyncCallback 的 Invoke 函数回调。
HRESULT CPlayer::CreateSession()
{
HRESULT hr = S_OK;
hr = CloseSession();
RETURN_IF_FAILED(hr);
RETURN_IF_FALSE(m_state == PlayerState_Closed);
hr = MFCreateMediaSession(NULL, &m_pSession);
RETURN_IF_FAILED(hr);
RETURN_IF_NULL(m_pSession);
m_state = PlayerState_Ready;
// designate this class as the one that will be handling events from the media session
hr = m_pSession->BeginGetEvent((IMFAsyncCallback*)this, NULL);
RETURN_IF_FAILED(hr);
CComPtr<IMFClock> pClock;
hr = m_pSession->GetClock(&pClock);
RETURN_IF_FAILED(hr);
m_pSessionClock.Release();
hr = pClock->QueryInterface(IID_IMFPresentationClock, (void**)&m_pSessionClock);
RETURN_IF_FAILED(hr);
return S_OK;
}
CTopoBuilder::CreateMediaSource 函数
首先创建 Topology 里的带头大哥 IMFMediaSource ~
这里 m_pSource 定义为 CComQIPtr<IMFMediaSource>,而 pSource 定义为 CComPtr<IUnknown>,通过 m_pSource = pSource 就完成了接口类型的转换,不明觉厉 *@[email protected]*
HRESULT CTopoBuilder::CreateMediaSource(PCWSTR sURL)
{
HRESULT hr = S_OK;
MF_OBJECT_TYPE objectType = MF_OBJECT_INVALID;
CComPtr<IMFSourceResolver> pSourceResolver;
CComPtr<IUnknown> pSource;
hr = MFCreateSourceResolver(&pSourceResolver);
RETURN_IF_FAILED(hr);
hr = pSourceResolver->CreateObjectFromURL(
sURL, // URL of the source.
MF_RESOLUTION_MEDIASOURCE | MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE,
// indicate that we want a source object, and pass in optional source search parameters
NULL, // Optional property store for extra parameters
&objectType, // Receives the created object type.
&pSource // Receives a pointer to the media source.
);
RETURN_IF_FAILED(hr);
m_pSource.Release();
m_pSource = pSource;
RETURN_IF_NULL(m_pSource);
return hr;
}
CTopoBuilder::CreateTopology 函数
创建 Topology,然后把 Source 里的每一条流都扯出来并尝试连接到一个合适的 Renderer(和相亲差不多 (^_^)∠※ )。
HRESULT CTopoBuilder::CreateTopology()
{
HRESULT hr = S_OK;
CComPtr<IMFPresentationDescriptor> pPresDescriptor;
DWORD nSourceStreams = 0;
m_pTopology.Release();
hr = MFCreateTopology(&m_pTopology);
RETURN_IF_FAILED(hr);
// Create the presentation descriptor for the media source - a container object that
// holds a list of the streams and allows selection of streams that will be used.
hr = m_pSource->CreatePresentationDescriptor(&pPresDescriptor);
RETURN_IF_FAILED(hr);
hr = pPresDescriptor->GetStreamDescriptorCount(&nSourceStreams);
RETURN_IF_FAILED(hr);
// For each stream, create source and sink nodes and add them to the topology.
for (DWORD i = 0; i < nSourceStreams; i++) {
hr = AddBranchToPartialTopology(pPresDescriptor, i);
// if we failed to build a branch for this stream type, then deselect it that will
// cause the stream to be disabled, and the source will not produce any data for it
if (FAILED(hr))
hr = pPresDescriptor->DeselectStream(i);
}
return S_OK;
}
CTopoBuilder::AddBranchToPartialTopology 函数
创建 SourceNode 和 OutputNode,配对,结束!
表面上简单粗暴,实际上 MF 后台还是做了不少牵线搭桥的事,包括插入 dumuxer,decoder,resampler 等等。
HRESULT CTopoBuilder::AddBranchToPartialTopology(IMFPresentationDescriptor* pPresDescriptor, DWORD nStream)
{
HRESULT hr = S_OK;
CComPtr<IMFStreamDescriptor> pStreamDescriptor;
CComPtr<IMFTopologyNode> pSourceNode;
CComPtr<IMFTopologyNode> pOutputNode;
BOOL streamSelected = FALSE;
hr = pPresDescriptor->GetStreamDescriptorByIndex(nStream, &streamSelected, &pStreamDescriptor);
if (streamSelected) {
hr = CreateSourceStreamNode(pPresDescriptor, pStreamDescriptor, pSourceNode);
hr = CreateOutputNode(pStreamDescriptor, m_videoHwnd, pOutputNode);
hr = m_pTopology->AddNode(pSourceNode);
hr = m_pTopology->AddNode(pOutputNode);
// Connect the source node to the sink node. The resolver will find the
// intermediate nodes needed to convert media types.
hr = pSourceNode->ConnectOutput(0, pOutputNode, 0);
}
return hr;
}
CTopoBuilder::CreateSourceStreamNode 函数
虽然 Source 对象早已经创建了,但 Topology 不接受,只认 Node,所以这里要穿上 Node 马甲。
HRESULT CTopoBuilder::CreateSourceStreamNode(
IMFPresentationDescriptor* pPresDescriptor,
IMFStreamDescriptor* pStreamDescriptor,
CComPtr<IMFTopologyNode> &pNode)
{
HRESULT hr = S_OK;
RETURN_IF_NULL(pPresDescriptor);
RETURN_IF_NULL(pStreamDescriptor);
CComPtr<IMFTopologyNode> pTempNode;
pNode = NULL;
// Create the topology node, indicating that it must be a source node.
hr = MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE, &pTempNode);
RETURN_IF_FAILED(hr);
// Associate the node with the source by passing in a pointer to the media source
// and indicating that it is the source
hr = pTempNode->SetUnknown(MF_TOPONODE_SOURCE, m_pSource);
RETURN_IF_FAILED(hr);
// Set the node presentation descriptor attribute of the node by passing
// in a pointer to the presentation descriptor
hr = pTempNode->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR, pPresDescriptor);
RETURN_IF_FAILED(hr);
// Set the node stream descriptor attribute by passing in a pointer to the stream descriptor
hr = pTempNode->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR, pStreamDescriptor);
RETURN_IF_FAILED(hr);
pNode = pTempNode;
return S_OK;
}
CTopoBuilder::CreateOutputNode 函数
有时候觉得 MF 挺扭捏的,创建个 Renderer 还要一个 IMFActivate 牵线。
HRESULT CTopoBuilder::CreateOutputNode(
IMFStreamDescriptor* pStreamDescriptor,
HWND hwndVideo,
CComPtr<IMFTopologyNode> &pNode)
{
HRESULT hr = S_OK;
CComPtr<IMFMediaTypeHandler> pHandler;
CComPtr<IMFActivate> pRendererActivate;
GUID majorType = GUID_NULL;
pNode = NULL;
RETURN_IF_NULL(pStreamDescriptor);
// Get the media type handler for the stream, which will be used to process
// the media types of the stream. The handler stores the media type.
hr = pStreamDescriptor->GetMediaTypeHandler(&pHandler);
RETURN_IF_FAILED(hr);
// Get the major media type (e.g. video or audio)
hr = pHandler->GetMajorType(&majorType);
RETURN_IF_FAILED(hr);
// Create an IMFActivate controller object for the renderer, based on the media type
// The activation objects are used by the session in order to create the renderers
// only when they are needed - i.e. only right before starting playback. The
// activation objects are also used to shut down the renderers.
if (majorType == MFMediaType_Audio)
hr = MFCreateAudioRendererActivate(&pRendererActivate);
else if (majorType == MFMediaType_Video)
hr = MFCreateVideoRendererActivate(hwndVideo, &pRendererActivate);
else
// fail if the stream type is not video or audio.
hr = E_FAIL;
RETURN_IF_FAILED(hr);
// Create the node that will represent the renderer
hr = MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &pNode);
RETURN_IF_FAILED(hr);
// Store the IActivate object in the sink node - it will be extracted later by the
// media session during the topology render phase.
hr = pNode->SetObject(pRendererActivate);
// if failed, clear the output parameter
if (FAILED(hr))
pNode = NULL;
RETURN_IF_FAILED(hr);
return hr;
}
IMFAsyncCallback::Invoke 回调函数
Media Session 的 所有事件 都会回调这里,记得最后要重新 BeginGetEvent,不然就傻傻等不到下一个事件了。
HRESULT CPlayer::Invoke(IMFAsyncResult* pAsyncResult)
{
CComPtr<IMFMediaEvent> pEvent;
HRESULT hr = S_OK;
MFUtil::AutoLock lock(m_critSec);
// Get the event from the event queue.
hr = m_pSession->EndGetEvent(pAsyncResult, &pEvent);
RETURN_IF_FAILED(hr);
// If the player is not closing, process the media event - if it is, do nothing.
if (m_state != PlayerState_Closed) {
hr = ProcessMediaEvent(pEvent);
RETURN_IF_FAILED(hr);
}
// If the media event is MESessionClosed, it is guaranteed to be the last event. If
// the event is MESessionClosed, ProcessMediaEvent() will return S_FALSE. In that
// case do not request the next event - otherwise tell the media session that this
// player is the object that will handle the next event in the queue.
if (hr != S_FALSE) {
hr = m_pSession->BeginGetEvent(this, NULL);
RETURN_IF_FAILED(hr);
}
return S_OK;
}
CPlayer::ProcessMediaEvent 函数
收好了,所有的 MF events 都在 这里。
注意:收到 MESessionClosed 事件后,函数返回 S_FALSE,上面的 Invoke 函数就不再监听下一个事件了(想监听也没用啊,人家都 Close 了)。
HRESULT CPlayer::ProcessMediaEvent(CComPtr<IMFMediaEvent>& pMediaEvent)
{
HRESULT hrStatus = S_OK; // Event status
HRESULT hr = S_OK;
UINT32 TopoStatus = MF_TOPOSTATUS_INVALID;
MediaEventType eventType;
RETURN_IF_NULL(pMediaEvent);
// Get the event status. If the operation that triggered the event did
// not succeed, the status is a failure code.
hr = pMediaEvent->GetStatus(&hrStatus);
RETURN_IF_FAILED(hr);
// Check if the async operation succeeded.
RETURN_IF_FAILED(hrStatus);
// Get the event type.
hr = pMediaEvent->GetType(&eventType);
RETURN_IF_FAILED(hr);
switch (eventType) {
case MESessionTopologyStatus:
// Get the status code.
hr = pMediaEvent->GetUINT32(MF_EVENT_TOPOLOGY_STATUS, (UINT32*)&TopoStatus);
BREAK_ON_FAIL(hr);
if (TopoStatus == MF_TOPOSTATUS_READY) {
m_state = PlayerState_Stopped;
hr = OnTopologyReady();
}
break;
case MEEndOfPresentation:
m_state = PlayerState_Stopped;
break;
case MESessionClosed:
m_state = PlayerState_Closed;
m_cvSessionStatus.wake();
hr = S_FALSE;
break;
case MESessionStarted:
hr = pMediaEvent->GetUINT64(MF_EVENT_PRESENTATION_TIME_OFFSET, (UINT64*)&m_hnsOffsetTime);
PRINT_ERROR_LOG_IF_FAILED(hr);
::PostMessage(g_hWnd, WM_MF_SESSION_PLAY, 0, 0);
break;
case MESessionStopped:
::PostMessage(g_hWnd, WM_MF_SESSION_STOP, 0, 0);
break;
case MESessionEnded:
::PostMessage(g_hWnd, WM_MF_SESSION_STOP, 0, 0);
break;
case MESessionCapabilitiesChanged:
::PostMessage(g_hWnd, WM_MF_CAPABILITIES_CHANGED, 0, 0);
break;
case MESessionNotifyPresentationTime:
hr = pMediaEvent->GetUINT64(MF_EVENT_PRESENTATION_TIME_OFFSET, (UINT64*) &m_hnsOffsetTime);
PRINT_ERROR_LOG_IF_FAILED(hr);
m_fReceivedTime = true;
break;
}
return hr;
}
CPlayer::PlayFrom 函数
兼顾 seek 和 resume 的功效。
HRESULT CPlayer::PlayFrom( MFTIME time )
{
HRESULT hr = S_OK;
PROPVARIANT var;
PropVariantInit( &var );
if ( PRESENTATION_CURRENT_POSITION == time ) {
var.vt = VT_EMPTY;
}
else {
var.vt = VT_I8;
var.hVal.QuadPart = time;
}
hr = m_pSession->Start( NULL, &var );
PRINT_ERROR_LOG_IF_FAILED(hr);
PropVariantClear( &var );
return hr;
}
MF 播放音频的 Topology
以下是播放一首 MP3 生成的 Topology,包含三个模块:Source, Decoder 和 Renderer。
– EOF –