Media Foundation播放器

发布时间 2023-03-22 19:14:13作者: 左眼水星

前文已经简单介绍了Microsoft Media Foundation。下面我们使用它来实现一个简单的视频播放器(MF要求使用C/C++,不提供.NET接口)。

初始化

在使用MF之前需要先初始化

 HRESULT LT = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
 MFStartup(MF_VERSION);

因为MF平台自身需要使用组件对象模型 (COM),所以需要先 用CoInitializeEx初始化COM,再调用MFStartup初始化MF平台。

既然有初始化过程,对应的也有销毁的过程(结束使用MF后)

MFShutdown();
CoUninitialize();

创建媒体源

第一步当然是指定MF要播放哪个文件。

MF_OBJECT_TYPE objectType = MF_OBJECT_INVALID;
IMFSourceResolver* pSourceResolver;
IUnknown* pSource;
MFCreateSourceResolver(&pSourceResolver);
pSourceResolver->CreateObjectFromURL(filepath,MF_RESOLUTION_MEDIASOURCE, nullptr ,&objectType, &pSource);
m_pSource = (IMFMediaSource*)pSource;

创建IMFSourceResolver对象并调用它的CreateObjectFromURL方法,传入文件路径filepath创建媒体源。得到的pSource指针指向的就是媒体源。

(注意:MF中的对象必须使用MFCreate******系列函数创建,后面可以看到一些其他的MFCreate函数)

然后我们把pSource转换成IMFMediaSource指针(m_pSource)。

我们知道大多数的视频文件中不仅有视频流还有音频流。它们是完全不同的数据结构,所以我们需要不同的逻辑来对它们进行处理。

IMFPresentationDescriptor* pPresDescriptor;
DWORD nSourceStreams{ 0 };
m_pSource->CreatePresentationDescriptor(&pPresDescriptor);
pPresDescriptor->GetStreamDescriptorCount(&nSourceStreams);
for ( DWORD x = 0; x < nSourceStreams; x++) {
    BuildTopology(pPresDescriptor, x);
}

使用CreatePresentationDescriptor函数获得媒体源的类型描述(文件容器描述)。

使用GetStreamDescriptorCount函数就可以知道媒体源中包含了几个媒体流,然后用for循环分别处理。

           注意:一个视频文件中只能确定至少包含一个视频流,可能有音频流(多数情况),可能有字幕流,并且视频流、音频流、字幕流都有可能包含多个(少数情况)。

IMFStreamDescriptor* pStreamDescriptor;
IMFTopologyNode* pSourceNode;
IMFTopologyNode* pOutputNode;
BOOL streamSelected = FALSE;
pPresDescriptor->GetStreamDescriptorByIndex(nStream, &streamSelected, &pStreamDescriptor);

调用GetStreamDescriptorByIndex函数获得流的属性描述,其中nStream表示流的索引(前面循环中的x)。pStreamDescriptor指向的就是流的属性描述。

获得IMFPresentationDescriptor对象和IMFStreamDescriptor对象是不能省略的是必不可少的,它们提供对于创建管道提供了必不可少的信息。

创建管道

MFCreateTopology(&m_pTopology);

使用MFCreateTopology函数创建管道对象。(再次提醒:MF中对象的创建必须调用MFCreate系列函数)

MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE, &pSourceNode);
pSourceNode->SetUnknown(MF_TOPONODE_SOURCE, m_pSource);
pSourceNode->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR, pPresDescriptor);
pSourceNode->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR, pStreamDescriptor);
m_pTopology->AddNode(pSourceNode);

使用MFCreateTopologyNode并指明MF_TOPOLOGY_SOURCESTREAM_NODE创建管道中的媒体源节点,然后使用SetUnknown设置节点的属性。媒体源指向前面创建的m_pSource,同时设置它的类型属性(描述文件的类型描述和媒体流的类型描述)。

调用AddNode把这个节点加入到指定的管道。至此媒体源已添加进管道中。

创建输出节点

前文提到MF平台支持将媒体流数据输出到文件、IO设备、网络等地方。这里我们就将流输出到电脑IO设备。

IMFMediaTypeHandler* pHandler;
IMFActivate* pRendererActivate{    nullptr };
GUID majorType = GUID_NULL;
pStreamDescriptor->GetMediaTypeHandler(&pHandler);
pHandler->GetMajorType(&majorType);
if (majorType == MFMediaType_Audio) {
    MFCreateAudioRendererActivate(&pRendererActivate);
}
else if (majorType == MFMediaType_Video) {
    MFCreateVideoRendererActivate(m_videoHwnd, &pRendererActivate);
}

从IMFStreamDescriptor获得IMFMediaTypeHandler对象,再调用GetMajorType即可获得流的MajorType,MajorType区分不同类型的流。

MFMediaType_Audio表示这是一个音频流,那就用MFCreateAudioRendererActivate创建表示音频渲染器

MFMediaType_Video表示这是一个视频流,那就用MFCreateVideoRendererActivate创建表示视频渲染器,其中m_videoHwnd代表窗口句柄。

然后把创建的pRendererActivate添加进管道。

MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &pOutputNode);
pOutputNode->SetObject(pRendererActivate);
m_pTopology->AddNode(pOutputNode);<br>pSourceNode->ConnectOutput(0, pOutputNode, 0) 

同样的创建管道节点使用MF_TOPOLOGY_OUTPUT_NODE标志着这是一个输出节点,并把它指向前面的pRendererActivate。然后调用AddNode将这个节点添加进管道,最后将源节点和输出节点相连。

至此我们就创建好了管道的输入节点和输出节点。

管道的作用

        我们知道文件中的数据不管是音频流还是视频流都是经过压缩(编码后)保存的,电脑IO设备只能识别原始的媒体数据流,所以我们必须先解码了之后才能使用。又因为数据的编码方案有多种,所以我们必须先得知道是哪种编码类型(ARGB32、H264等)才能去找对应的解码器解码媒体流。但是这些工作管道会帮助我们完成,这就是管道最重要的功能:解析管道。管道会查找正确的解码器组件并加入到管道中补全管道。

       原理就是管道会自我分析自身中的节点(组件),在视频文件播放器这个例子中,从媒体源中读取得到的是编码过的媒体流而IO设备只能识别原始的媒体数据流,所以管道能分析出这两个节点(组件)无法直接连在一起(组件的处理模式已在前文中介绍),必须找到这样一个组件:它的输入流是编码后的媒体流,它的输出是原始媒体流(也就是解码器类组件)然后把他放在媒体源组件和媒体呈现组件之间,这样就能正确处理了。而管道要找到这样的组件则必须要知道媒体源输出的数据流是什么,所以前面的代码中设置IMFPresentationDescriptor和IMFStreamDescriptor就是在告诉管道这些关于媒体源的数据流信息。

创建MFSession

MFCreateMediaSession(  nullptr , &m_pSession);
m_pSession->SetTopology(0, m_topoBuilder.m_pTopology);

 MFCreateMediaSession函数创建Session,然后使用SetTopology设置为我们刚创建完成的管道。

Session的作用前文已经提到:管理管道中媒体数据的流动。调用Session的Start方法开始启动数据流流动,在这个例子中就是开始播放视频。调用Pause方法暂停数据流动,在这个例子中就是暂停视频播放。

此外Session还可以实现倍数播放(也就是控制数据的流动速度),以及从指定位置开始播放等功能。

但是Session最重要的功能是数据流同步功能和资源管理功能。前面已经提到一个视频文件中有视频流和音频流,数据同步功能就是让视频流和音频流能够同步流动,资源管理就是Session负责创建和销毁管道中涉及到的对象(前面创建管道中的节点都是延迟创建,真正的创建由Session来管理)。

总结:

播放器的功能需要不同的组件配合完成任务,把组件装入管道中后,管道可以帮助我们分析是否缺少组件,如缺少会自动帮我们找到正确的组件补全管道。

使用IMSession管理创建好的IMFTopology可以更加方便的实现播放、暂停和其他高级功能等。

完整代码:https://github.com/lxy20221110/MMediaFoundation