音视频播放 via Media Foundation I

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 上推出第二种编程模型,内含 SourceReaderTranscode APISinkWriterMFPlay 等高度封装模块,大大简化了 MF 的使用难度。

# 本文使用了第一种(复杂的)编程模型。

Media Foundation 播放音视频

播放流程图

MF session play audio

播放代码

以下是整个播放过程的概要代码,略去错误处理和一些函数的具体实现:

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 SessionTopology(相当于 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 的消息将通过 IMFAsyncCallbackInvoke 函数回调。

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 就完成了接口类型的转换,不明觉厉 *@_@*

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。
mf audio play topology

MF 播放视频的 Topology

以下是播放一个 WMV 文件生成的 Topology,包含五个模块:Source, Audio Decoder, Audio Render, Video Decoder 和 Video Render 。
mf v play topo

其他框架下的播放

音频播放其他实现方式:

视频播放其他实现方式:

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值