Media Foundation 入门:用 COM 视角理解 API
· 更新日期: · 小村 豪 · Media Foundation, COM, C++, Windows开发
更新记录(3 条,最后更新 2026年09月03日)
本文的修改记录。已保存的更新前版本,可通过带有 DOI 的永久链接阅读。
- 本文此前是日文原文的节译,缺少大量章节、表格、Mermaid 图、图题、脚注与 FAQ。现已改写为日文原文的完整译文,技术主张与日文版一致,并补上了此前缺失的图与表,同时统一了全篇的术语译法。 查看更新前的版本 (DOI: 10.5281/zenodo.22276568)
- 补充了日文原文中已有的咨询引导(consultation_services)。正文内容没有改动。
- 修复了参考链接等处含有竖线(管道)符号的行被渲染为表格、导致链接无法点击的显示错误。正文内容未作改动。
- 首次发布
引用本文(DOI: 10.5281/zenodo.21615428)
本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。
小村 豪(2026)。《Media Foundation 入门:用 COM 视角理解 API》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615428 https://comcomponent.com/zh-CN/blog/2026/03/09/002-media-foundation-why-it-feels-like-com/
- DOI(最新版本)
- 10.5281/zenodo.21615428
- DOI(此版本)
- 10.5281/zenodo.22281931
刚开始接触 Media Foundation 时,很容易有这样的感觉:明明是在用 Windows 的视频、音频 API,怎么突然冒出这么多 COM 相关的内容。CoInitializeEx、MFStartup、IMFSourceReader、IMFMediaType、IMFTransform、IMFActivate、HRESULT、GUID 等概念一下子全部涌出来,整体风格瞬间变得很像 Win32 / COM,反而让人看不清 Media Foundation 到底是什么。
本文不打算把 Media Foundation 像字典一样全面覆盖,而是聚焦在以下 3 点:
- 为什么使用 Media Foundation 时会自然地涉及 COM
- 哪些地方 COM 的色彩会变浓
- 最开始应该从 Source Reader / Sink Writer / Media Session / MFT 中的哪一个入手
代码示例基于 C++,但即便是通过 .NET 等语言经由封装层来使用,思路本身也基本相同。
目录
- 先说结论(一句话)
- 术语与整体图景
- 2.1. 先掌握含义的几个术语
- 2.2. Media Foundation 的整体图景(图)
- Media Foundation 显出 COM 一面的地方
- 3.1. 初始化时
CoInitializeEx与MFStartup并列出现 - 3.2. 对象的传递以接口为中心
- 3.3. 配置与类型信息以
IMFAttributes和 GUID 为中心 - 3.4. Activation Object 的出现
- 3.5. 异步、回调、线程的处理方式也很 COM
- 3.1. 初始化时
- 但 Media Foundation 并不等于 COM
- 从哪里入手(如何选择入口)
- 5.1. 先从 Source Reader 入手的情况
- 5.2. 要写入文件就用 Sink Writer
- 5.3. 要处理播放和同步就用 Media Session
- 5.4. 要插入自定义部件就用 MFT
- 实务检查清单
- 总结
- 参考资料
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 21 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
1. 先说结论(一句话)
- Media Foundation 是用于处理视频和音频的平台,并不是说整个 API 直接等同于纯粹的 COM
- 不过,source / transform / sink / activation / attributes / callback 之间的边界都是用 COM 接口表示的,因此使用过程中自然会涉及
IUnknown、HRESULT、GUID、apartment - 先从 Source Reader / Sink Writer 入手,需要播放控制时再进阶到 Media Session,需要自定义转换器时再进阶到 MFT,这样理解起来会更顺畅
简而言之,Media Foundation 是一个媒体处理平台,COM 深深嵌入在它的边界处。
先掌握这一点,理解“为什么会突然显出 COM 的一面”就会容易得多。
flowchart TB
accTitle: Media Foundation与COM的关系
accDescr: 说明Media Foundation本身是媒体处理平台,整个API并非纯粹的COM,但部件之间的边界用COM接口表示,因此IUnknown、HRESULT、GUID等话题会自然出现。
mf["Media Foundation"] --> plat["媒体处理平台"]
mf --> border["部件的边界是COM接口"]
border --> com["出现IUnknown、HRESULT、GUID"]
plat -.-> note["整个API并非纯粹的COM"]
图1:本体是媒体处理平台,COM 深深嵌入在它的边界处。
2. 术语与整体图景
在进入 COM 的话题之前,先把本文使用的术语和 Media Foundation 的大致框架过一遍。
2.1. 先掌握含义的几个术语
| 术语 | 这里的含义 |
|---|---|
| Media Source | 把媒体数据送入管道的入口。可以是文件、网络、采集设备等 |
| MFT | Media Foundation Transform。解码器、编码器、视频转换器等的通用模型 |
| Media Sink | 媒体数据的去处。屏幕显示、音频输出、写入文件等 |
| Media Session | 管理整个管道流转的机制,负责播放和同步 |
| Topology | 表示 source / transform / sink 如何连接的连接图 |
| Activation Object | 用于稍后创建本体的辅助对象,由 IMFActivate 表示 |
| Attributes | 以 GUID 为键的 key/value 存储,在整个 Media Foundation 中被大量使用 |
| apartment | COM 用来归拢线程的单位。它规定“这个对象可以从哪些线程调用”,由 CoInitializeEx 的参数决定(3.1、3.5) |
| STA / MTA | apartment 的种类。STA(Single-Threaded Apartment)绑定到一条线程,来自其他线程的调用要经消息泵转入。MTA(Multi-Threaded Apartment)由多条线程共享同一个 apartment,可以直接调用。详见 避免 COM 的 STA/MTA 导致挂起的基础知识 |
| work queue | Media Foundation 为驱动异步处理而持有的线程机制。callback 就是从这里的线程调用过来的(3.5) |
提前把这些当作术语掌握下来,读文档时的卡顿会大幅减少。
apartment 会在 3.5 展开讲,这里只需记住“Media Foundation 的 callback 可能来自与调用 ReadSample 的线程不同的另一条线程(MTA 的 work queue)”就够了。
2.2. Media Foundation 的整体图景(图)
从大局上看,Media Foundation 主要是 关于媒体管道的话题。 COM 固然重要,但先看整体图景会更容易理清脉络。
flowchart TB
subgraph Pipeline["使用整个管道的模型"]
Source1["Media Source"] --> Transform1["MFT"]
Transform1 --> Sink1["Media Sink"]
Session["Media Session"] --- Source1
Session --- Transform1
Session --- Sink1
end
subgraph Direct["应用直接处理数据的模型"]
Source2["Media Source"] --> Reader["Source Reader (+ decoder)"]
Reader --> App["应用程序"]
App --> Writer["Sink Writer (+ encoder)"]
Writer --> Sink2["Media Sink"]
end
图2:两种使用方式。把管道交给 Media Session 的模型,以及由应用通过 Reader / Writer 直接处理数据的模型。
Media Foundation 大致有以下两种使用方式:
- 使用整个管道的模型
- 连接 source / transform / sink,由 Media Session 管理数据流和 A/V 同步
- 应用直接处理数据的模型
- 用 Source Reader 从 source 取出数据,用 Sink Writer 写入 sink
后者在希望自己处理帧或采样数据的场景下更容易入手。 而如果希望连播放和同步都交给平台处理,前者才是正统做法。
需要牢记的是,Media Foundation 的本质是一个媒体处理平台,与直接操作一堆 COM 对象的感觉略有不同。
不过,一旦开始关注这些部件之间的边界,COM 的一面就会突然变浓。下一章会依次介绍这些位置。
3. Media Foundation 显出 COM 一面的地方
COM 色彩变浓的位置,大致可以归纳为以下 5 处。
| 位置 | 会出现什么 | 首先要理解的点 |
|---|---|---|
| 3.1. 初始化 | CoInitializeEx, MFStartup |
COM 初始化与 Media Foundation 初始化是两件不同的事 |
| 3.2. 对象创建与传递 | IMFSourceReader, IMFMediaType, IMFTransform |
大多是接口指针 + HRESULT |
| 3.3. 配置 | IMFAttributes, GUID |
配置值和类型信息以 key/value + GUID 的形式表达 |
| 3.4. 枚举与延迟创建 | IMFActivate, ActivateObject |
枚举结果本身有时并不是本体 |
| 3.5. 异步 | IMFSourceReaderCallback, work queue |
需要意识到 callback 与 apartment 的关系 |
| (第 4 章)播放控制 | topology, Media Session | 整个管道的流转是 Media Foundation 特有的概念 |
只有最后的播放控制性质不同,它不属于 COM 的通用话题,而是 Media Foundation 自身的功能。因此放到第 4 章单独讨论。
下面依次来看。代码不是完整示例,只放 足以看出哪里会显出 COM 一面的摘录。
3.1. 初始化时 CoInitializeEx 与 MFStartup 并列出现
这是大多数人最先感到别扭的地方。在谈到打开文件、从摄像头取数据之前,首先出现的是 CoInitializeEx 和 MFStartup。
CoInitializeEx用于初始化 COM 库MFStartup用于初始化 Media Foundation 平台
也就是说,仅完成 COM 初始化是不够的,还需要单独初始化 Media Foundation。 到这里就能明白,“这不只是一个普通的视频 API,底层还嵌入了相当多基于 COM 的契约”。
template <class T>
void SafeRelease(T** pp)
{
if (pp != nullptr && *pp != nullptr)
{
(*pp)->Release();
*pp = nullptr;
}
}
HRESULT InitializeMediaFoundationForCurrentThread()
{
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (FAILED(hr))
{
return hr;
}
hr = MFStartup(MF_VERSION);
if (FAILED(hr))
{
CoUninitialize();
return hr;
}
return S_OK;
}
void UninitializeMediaFoundationForCurrentThread()
{
MFShutdown();
CoUninitialize();
}
CoInitializeEx 与 MFStartup 并列出现的这种写法,正是使用 Media Foundation 时 COM 气息突然变浓的最初位置。
flowchart TB
accTitle: 初始化与终止的两段式结构
accDescr: 说明先用CoInitializeEx初始化COM库,再用MFStartup初始化Media Foundation平台后才开始使用,终止时按相反顺序调用MFShutdown和CoUninitialize的两段式结构。
co["CoInitializeEx(COM的初始化)"] --> mfs["MFStartup(MF的初始化)"]
mfs --> use["使用Media Foundation"]
use --> shut["MFShutdown"]
shut --> coun["CoUninitialize"]
图3:仅完成 COM 的初始化是不够的。初始化分两段,终止时按相反顺序回收。
在实务中,此时先确定好以下几点会让后续更轻松。
- 由哪个线程使用 Media Foundation
- 该线程设为 STA 还是 MTA
- 由谁负责
MFStartup/MFShutdown与CoInitializeEx/CoUninitialize的职责
在实际实现中,有时其他层已经负责了 COM 初始化。即便如此,提前固定由谁承担这项职责 也更安全。如果这部分设计一直含糊不清,之后在处理 callback 和 UI 联动时就会变得难以理清。
另外,本文的代码自己写了 SafeRelease,用裸接口指针来管理对象。这是因为 Microsoft 文档中的示例就是这种写法,能看清 AddRef / Release 分别在哪里起作用。不过,这并不构成实务代码里不使用智能指针的理由。如果是用 C++ 新写代码,统一到下面两者之一会更安全。
| 选项 | 实体 | 备注 |
|---|---|---|
Microsoft::WRL::ComPtr<T> |
<wrl/client.h> |
随 Windows SDK 一起提供,不需要额外依赖。用 Get() 取裸指针,用 GetAddressOf() / & 传 out 参数,用 As<U>() 写 QueryInterface |
wil::com_ptr<T> |
WIL(Windows Implementation Libraries)的 wil/com.h |
需要通过 NuGet 等方式单独引入。可以与把 HRESULT 转换为异常的辅助工具配合使用 |
改用 ComPtr 之后,3.1 后半出现的那种 goto done; 与 SafeRelease 的组合就不再需要,离开作用域时会自动 Release。本文为了让 COM 的写法可见而保留了裸指针,但 新代码建议从 ComPtr 开始。
flowchart TB
accTitle: 裸指针与智能指针的取舍
accDescr: 说明本文代码为了让AddRef和Release的作用点可见而使用裸指针与SafeRelease,而实务中的新代码统一到ComPtr或wil::com_ptr后,离开作用域时会自动Release,因而更安全。
raw["裸指针与SafeRelease"] -.-> why["能看清Release作用点的写法"]
smart["ComPtr或wil::com_ptr"] --> auto["离开作用域即Release"]
auto --> rec["新代码从ComPtr开始"]
图4:本文的代码出于学习目的保留裸指针。实务中的新代码应统一到智能指针。
3.2. 对象的传递以接口为中心
阅读 Media Foundation 的 API 时会发现,大部分返回值和 out 参数都是 COM 接口。
IMFSourceReaderIMFMediaTypeIMFTransformIMFActivateIMFSampleIMFMediaBuffer
特别之处在于,不仅数据本体,连类型信息和配置对象也都用接口来表示。
例如:
IMFTransform是表示 MFT 的接口IMFAttributes是一个 key/value 存储IMFMediaType是继承自IMFAttributes的“媒体格式说明”
像 media type 这类“感觉上属于配置数据”的东西,也是用 COM 接口来承载的。到这里,IUnknown、QueryInterface、AddRef / Release、HRESULT 这些上下文自然就出现了。
flowchart TD
IUnknown["IUnknown"]
IUnknown --> IMFAttributes["IMFAttributes"]
IMFAttributes --> IMFMediaType["IMFMediaType"]
IMFAttributes --> IMFActivate["IMFActivate"]
IUnknown --> IMFSourceReader["IMFSourceReader"]
IUnknown --> IMFTransform["IMFTransform"]
图5:主要接口的谱系。连配置和类型信息也用以 IUnknown 为顶点的 COM 接口来表示。
看到这里就能明白,“Media Foundation 是媒体 API,但边界的表达方式相当 COM 化”。
3.3. 配置与类型信息以 IMFAttributes 和 GUID 为中心
使用 Media Foundation 时,会有一个瞬间让人觉得配置突然变得全是 GUID。其核心就是 IMFAttributes,一个以 GUID 为键的 key/value 存储。它在整个 Media Foundation 中被大量使用。
其中特别重要的是 IMFMediaType,它继承自 IMFAttributes,把媒体格式信息作为属性来持有。
例如以下信息。
- major type(音频还是视频)
- subtype(H.264、AAC、RGB32、PCM 等)
- 帧尺寸
- 帧率
- 采样率
- 通道数
flowchart LR
MediaType["IMFMediaType"] --> Major["MF_MT_MAJOR_TYPE"]
MediaType --> Subtype["MF_MT_SUBTYPE"]
MediaType --> Detail["尺寸 / FPS / 采样率等"]
图6:IMFMediaType 是属性存储,用 GUID 键持有 major type、subtype 等格式信息。
这里很容易被感觉成“GUID 的森林”,但实际做的事情相当直白。
- 用属性存储来保存配置
- media type 也用属性存储来表示
- source / transform / sink 之间通过查看这些属性来协调格式
只是配置和类型信息的表达方式用了 COM 式的接口与 GUID,仅此而已。
把上面的内容落到代码上,就是下面这样。这是一个只用 Source Reader 从视频中读取 1 帧的例子。
HRESULT ReadOneVideoSample(PCWSTR path)
{
IMFSourceReader* pReader = nullptr;
IMFMediaType* pType = nullptr;
IMFSample* pSample = nullptr;
HRESULT hr = MFCreateSourceReaderFromURL(path, nullptr, &pReader);
if (FAILED(hr)) goto done;
hr = MFCreateMediaType(&pType);
if (FAILED(hr)) goto done;
hr = pType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
if (FAILED(hr)) goto done;
hr = pType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
if (FAILED(hr)) goto done;
hr = pReader->SetCurrentMediaType(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
nullptr,
pType);
if (FAILED(hr)) goto done;
DWORD streamFlags = 0;
LONGLONG timestamp = 0;
hr = pReader->ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
0,
nullptr,
&streamFlags,
×tamp,
&pSample);
if (FAILED(hr)) goto done;
// 从 pSample 中取出 IMFMediaBuffer 并处理
done:
SafeRelease(&pSample);
SafeRelease(&pType);
SafeRelease(&pReader);
return hr;
}
从这里可以看出以下几点。
- reader 和 media type 都是 COM 接口
- 配置是基于 GUID 的
- 返回值是
HRESULT - 在同步模式下,
ReadSample会阻塞
哪怕“只想读取 1 帧”,在 Media Foundation 的边界处也会呈现出相当 COM 化的一面。最后提到的同步模式会在 3.5 讨论。
media type negotiation 的步骤(这是第 6 章检查清单里列为“最先要确认的 3 点”之一的条目)
上面的代码只是声明了“我要 RGB32”,实际实务中在它前后还需要一套步骤。对于 Source Reader,Microsoft 文档给出的流程是下面 4 步。
- 枚举原生类型 — 调用
IMFSourceReader::GetNativeMediaType(streamIndex, typeIndex, &pType),把typeIndex从 0 开始递增。超出范围时会返回MF_E_NO_MORE_TYPES,那就是枚举的终点(如果streamIndex超出范围则返回MF_E_INVALIDSTREAMNUMBER)。文件通常每个流只有 1 种类型,而摄像头会有多种格式 - 确认 major type — 从枚举得到的 media type 中读取
MF_MT_MAJOR_TYPE,判断是音频还是视频。不看这一项就按固定值推进,等于把视频的配置投给了音频流 - 组装并设置想要的输出格式 — 用
MFCreateMediaType新建一个 media type,设置MF_MT_MAJOR_TYPE和MF_MT_SUBTYPE后调用SetCurrentMediaType。如果想保持压缩状态接收,就直接传入步骤 1 得到的类型;如果希望解码,就指定未压缩的格式(MFVideoFormat_RGB32、MFAudioFormat_PCM等)。解码器由 Source Reader 自动加载 - 重新读取最终确定的格式 — 在
SetCurrentMediaType之后调用GetCurrentMediaType,取得实际确定下来的格式细节(帧尺寸、stride、采样率等)。步骤 3 传入的只是部分指定,所以 确定值要由这一步来读 才是正确顺序
如果跳过这 4 步,抱着“大概是这个格式吧”推进,要么会收到 MF_E_INVALIDMEDIATYPE,要么即使通过了也会读到与预期不同格式的缓冲区。
flowchart TB
accTitle: media type negotiation的4个步骤
accDescr: 说明先用GetNativeMediaType枚举原生类型,确认major type,组装想要的输出格式并用SetCurrentMediaType设置,最后用GetCurrentMediaType重新读取最终确定格式的4个步骤。
s1["用GetNativeMediaType枚举"] --> s2["确认major type"]
s2 --> s3["用SetCurrentMediaType设置想要的格式"]
s3 --> s4["用GetCurrentMediaType读取确定值"]
s1 -.-> stop["MF_E_NO_MORE_TYPES是枚举的终点"]
图7:格式协调分 4 步。传入的只是部分指定,所以最后要重新读取确定值。
3.4. Activation Object 的出现
Media Foundation 的 COM 味特别浓的地方,就是 activation object。
IMFActivate 是用于稍后创建本体的辅助对象。把它类比为 COM 中的 class factory,理解起来会更容易。
在出现它的场景里,枚举 API 的返回值有时不是“可以直接使用的本体”,而先是一组 IMFActivate* 数组。
然后,只对需要的那个调用 ActivateObject 来实例化。
sequenceDiagram
participant App as 应用程序
participant Enum as 枚举API
participant Act as IMFActivate
participant Obj as IMFTransform / Sink 等
App->>Enum: 调用枚举
Enum-->>App: IMFActivate* 数组
App->>Act: 确认属性
App->>Act: ActivateObject(...)
Act-->>App: 实体 COM 对象
图8:枚举 API 返回的是 IMFActivate,调用 ActivateObject 之后才得到实体的 COM 对象。
这种形式与 Media Foundation 事后寻找并组合可替换部件的设计 很契合。
另外,activation object 本身也可以持有 attributes,所以经常会出现“先查看候选的属性”“按需设置”“稍后再实例化”这样的流程。这一点也相当 COM 化。
实际用 MFTEnumEx 枚举 MFT 并实例化,写出来是下面这样。
HRESULT FindH264Decoder(IMFTransform** ppTransform)
{
*ppTransform = nullptr;
IMFActivate** ppActivate = nullptr;
UINT32 count = 0;
MFT_REGISTER_TYPE_INFO inputType = {};
inputType.guidMajorType = MFMediaType_Video;
inputType.guidSubtype = MFVideoFormat_H264;
HRESULT hr = MFTEnumEx(
MFT_CATEGORY_VIDEO_DECODER,
MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_LOCALMFT,
&inputType,
nullptr,
&ppActivate,
&count);
if (FAILED(hr))
{
return hr;
}
if (count == 0)
{
CoTaskMemFree(ppActivate);
return MF_E_TOPO_CODEC_NOT_FOUND;
}
hr = ppActivate[0]->ActivateObject(
__uuidof(IMFTransform),
reinterpret_cast<void**>(ppTransform));
for (UINT32 i = 0; i < count; ++i)
{
ppActivate[i]->Release();
}
CoTaskMemFree(ppActivate);
return hr;
}
枚举结果一开始并不是 IMFTransform*,而是以 IMFActivate** 返回,调用 ActivateObject 之后才终于取得实体的 IMFTransform。这个流程相当好地体现了 Media Foundation“突然显出 COM 一面”的感觉。
flowchart TB
accTitle: 从MFTEnumEx到解码器实例化的流程
accDescr: 说明用MFTEnumEx枚举解码器候选会返回IMFActivate数组,没有候选时返回MF_E_TOPO_CODEC_NOT_FOUND,有候选时用ActivateObject实例化取得IMFTransform,之后对每个IMFActivate调用Release并用CoTaskMemFree释放数组本身的流程。
enum["用MFTEnumEx枚举候选"] --> arr["返回IMFActivate数组"]
arr --> q{"是否有候选"}
q -->|"没有"| nf["MF_E_TOPO_CODEC_NOT_FOUND"]
q -->|"有"| act["用ActivateObject实例化"]
act --> obj["取得IMFTransform"]
act -.-> free["对每个元素调用Release"]
free -.-> free2["数组用CoTaskMemFree释放"]
图9:枚举→确认候选→实例化→释放的流程。枚举结果并不是可以直接使用的本体。
3.5. 异步、回调、线程的处理方式也很 COM
在 Media Foundation 的实务中容易被忽略的是异步处理和线程模型。
例如,Source Reader 默认是同步模式。在同步模式下,ReadSample 会阻塞。
根据文件、网络或设备的状态,这个等待有时会变成明显可见的时长。
如果想切换到异步模式,需要在创建 Source Reader 时传入 callback。
流程是:准备一个实现了 IMFSourceReaderCallback 的对象,把它设置到 MF_SOURCE_READER_ASYNC_CALLBACK 属性中,然后再创建。
HRESULT CreateSourceReaderAsync(
PCWSTR path,
IMFSourceReaderCallback* pCallback,
IMFSourceReader** ppReader)
{
IMFAttributes* pAttributes = nullptr;
HRESULT hr = MFCreateAttributes(&pAttributes, 1);
if (FAILED(hr))
{
return hr;
}
hr = pAttributes->SetUnknown(MF_SOURCE_READER_ASYNC_CALLBACK, pCallback);
if (SUCCEEDED(hr))
{
hr = MFCreateSourceReaderFromURL(path, pAttributes, ppReader);
}
SafeRelease(&pAttributes);
return hr;
}
也就是说,
- callback 本身就是 COM 接口
- 异步设置是通过
IMFAttributes完成的 - 模式在创建时就已确定
是这样一种形式。
另外一个比较重要的点是 apartment。 Media Foundation 的异步处理使用 work queue,work queue 所在的线程是 MTA。 因此,把应用侧也统一到 MTA 会让实现更简单。
sequenceDiagram
participant App as 应用线程
participant Reader as Source Reader
participant Queue as MF work queue (MTA)
participant Cb as IMFSourceReaderCallback
App->>Reader: ReadSample(...)
Reader-->>App: 立即返回
Reader->>Queue: 在内部处理
Queue->>Cb: OnReadSample(...)
图10:异步模式下 ReadSample 会立即返回,OnReadSample 由 work queue 的线程调用。
在 callback 相关的地方,需要注意的是这几点。
- 不要在 callback 中直接操作 UI 线程上的 STA 对象
- callback 的实现必须是 线程安全 的
- 如果需要更新 UI,应该只把结果传回 UI 线程
- 一开始就把“Media Foundation 的 callback 会从哪个线程过来”固定下来再考虑
Media Foundation 并不会自动帮你妥善处理 STA 对象的问题。 因此,把使用 Media Foundation 的 worker 统一到 MTA,并与 UI 之间明确搭建桥梁,会更容易理清结构。
flowchart TB
accTitle: callback与UI线程之间如何搭桥
accDescr: 说明callback来自MTA的work queue线程,因此实现要保证线程安全,不要直接操作STA的UI对象,需要更新UI时只把结果传回UI线程的梳理方式。
cb["callback来自MTA的work queue"] --> safe["实现要保证线程安全"]
cb -.-> ng["不直接操作STA的UI对象"]
safe --> bridge["只把结果传回UI线程"]
图11:把使用 Media Foundation 的 worker 统一到 MTA,并与 UI 之间明确搭桥。
4. 但 Media Foundation 并不等于 COM
读到这里,很容易产生“说到底 Media Foundation 就是 COM 本身”的想法。 但这一点略有偏差。
Media Foundation 中存在一些无法只用 COM 的通用理论来解释的、平台特有的概念。
MFStartup/MFShutdown- Media Session
- topology
- topology loader
- presentation clock
- Source Reader / Sink Writer
这些都属于 如何驱动媒体管道 这一 Media Foundation 自身的职责范围。
例如在 Media Session 中,应用程序传入 partial topology 后,topology loader 会补充所需的 transform,将其解析为 full topology。 这并不是 COM 的通用话题,而是 Media Foundation 作为媒体处理平台所具备的功能。
flowchart LR
Partial["Partial Topology<br/>Source -> Output"] --> Loader["Topology Loader"]
Loader --> Full["Full Topology<br/>Source -> Decoder MFT -> Output"]
图12:传入 partial topology 后,topology loader 会补充所需的 transform,将其解析为 full topology。
Media Foundation 是 一方面用 COM 表达部件之间的契约,另一方面在其之上作为媒体处理平台运行 的产物。用这种两段式结构来看待它,就不容易迷失方向。
flowchart TB
accTitle: COM层与平台层的两段式结构
accDescr: 说明Media Foundation用COM表达部件之间的契约,并在其之上拥有Media Session、topology、presentation clock等媒体管道特有的机制,是两段式的结构。
com2["COM的层(表达部件的契约)"] --> mf2["媒体处理平台的层"]
mf2 --> own["Media Session和topology等"]
own -.-> note2["无法只用COM通论解释的MF特有概念"]
图13:它不是 COM 的翻版。在 COM 的层之上,还叠着驱动管道的 MF 特有的层。
5. 从哪里入手(如何选择入口)
在决定最初的入口时,下面这张图往往就足够了。
flowchart TD
Start["想做的事"] --> Q1{"最先需要什么?"}
Q1 -- "想读取帧 / 采样" --> A1["Source Reader"]
Q1 -- "想写入文件" --> A2["Sink Writer"]
Q1 -- "需要播放控制或 A/V 同步" --> A3["Media Session"]
Q1 -- "想插入自定义转换器" --> A4["MFT"]
图14:从最先需要的东西出发选择入口。想读就用 Reader,想写就用 Writer,要播放就用 Session。
整理成表格,就是下面这样。
| 想做的事 | 首先接触的对象 | COM 浓度 | 补充 |
|---|---|---|---|
| 想从文件或摄像头中取出帧 / 采样 | Source Reader | 中 | 如果需要,还会帮你处理 decoder |
| 想把生成的音频 / 视频写入文件 | Sink Writer | 中 | 如果需要,可以一并处理 encoder 和 media sink |
| 想处理播放、停止、跳转、A/V 同步、质量控制 | Media Session | 高 | 需要理解 topology 与 session |
| 想插入自定义的转换器或类似 codec 的部件 | MFT | 高 | 以 IMFTransform 为核心来思考 |
| 想先看枚举出来的候选,再只实例化需要的那个 | IMFActivate |
高 | 有时返回的不是本体,而是 activation object |
5.1. 先从 Source Reader 入手的情况
如果想从文件或设备中取出数据,Source Reader 作为入口相当容易使用。
比如下面这些场景比较适合。
- 想从视频文件中取出帧
- 想解码音频文件并取出采样
- 想从摄像头取出帧
- 想把 Media Foundation 的 source 接入自己的处理管道
Source Reader 会按需加载 decoder,并把数据传递给应用程序。 但另一方面,它不会管理 presentation clock、A/V 同步,更不负责画面渲染本身。
理解为 “不是用来播放,而是用来取数据”的入口,会更容易把握。
flowchart TB
accTitle: Source Reader的职责范围
accDescr: 说明Source Reader从文件或摄像头取出数据,按需加载decoder并交给应用,但不负责presentation clock的管理、A/V同步和画面渲染的职责范围。
src["文件、摄像头等source"] --> sr["Source Reader"]
sr --> app["把数据交给应用"]
sr -.-> dec["需要时加载decoder"]
sr -.-> not["不负责播放和同步"]
图15:Source Reader 是“不是用来播放,而是用来取数据”的入口。
5.2. 要写入文件就用 Sink Writer
Sink Writer 是想把音频或视频写入文件时的入口。
典型用途包括这些。
- 想把生成的帧保存为视频文件
- 想编码音频采样并写出
- 想把读取的数据转换成其他格式后保存
Sink Writer 会按需查找并加载 encoder,并管理向 media sink 的数据流转。 它常与 Source Reader 组合使用,但两者是各自独立的部件,并不一定要成对使用。
flowchart TB
accTitle: Sink Writer的职责范围
accDescr: 说明应用把生成的帧或音频采样交给Sink Writer后,它会按需查找并加载encoder,管理向media sink的数据流转并写入文件。
app2["应用生成的帧、音频"] --> sw["Sink Writer"]
sw -.-> enc["需要时加载encoder"]
sw --> sink["写出到media sink"]
sw -.-> ind["与Source Reader相互独立的部件"]
图16:Sink Writer 是负责编码与写出的入口。与 Reader 成对使用并非必需。
5.3. 要处理播放和同步就用 Media Session
如果不只是“想从文件里取数据”,而是 想认真地播放,那么以 Media Session 为核心来考虑会更顺理成章。
在有以下需求时,就是 Media Session 登场的时候。
- 想处理播放 / 停止 / 跳转
- 想把音视频同步交给平台处理
- 想连质量控制和格式变化都纳入管道来处理
- 想用 topology 来组织 source / transform / sink 的流转
进入这一层后,会比 Source Reader / Sink Writer 更接近“Media Foundation 本体”。 相应地,topology、session event 等 Media Foundation 特有的概念也会增多。
flowchart TB
accTitle: 选择Media Session的判断
accDescr: 说明如果想把播放、停止、跳转、A/V同步和质量控制都交给平台,就以Media Session为核心来考虑,用topology组织流转,相应地Media Foundation特有的概念也会增多。
need["想把播放、跳转、同步都交出去"] --> ms["以Media Session为核心考虑"]
ms --> topo["用topology从source组到sink"]
ms -.-> deep["MF特有的概念相应增多"]
图17:不是“想取数据”,而是“想认真地播放”时,Media Session 才是正路。
5.4. 要插入自定义部件就用 MFT
MFT 是 Media Foundation 中 transform 的通用模型。
进入这里的场景是这些。
- 想自己实现解码器或编码器
- 想把视频处理或音频处理部件插入管道
- 想枚举 codec 或转换器,并自行挑选
- 想比默认的自动解析进行更深入的控制
在 MFT 的世界里,IMFTransform、IMFActivate、media type negotiation、采样 / 缓冲区管理等 COM 式契约会非常突出。
因此,与一开始就直接进入 MFT 相比,先看清 Source Reader / Sink Writer / Media Session 中哪一个才是真正需要的,会更容易理解。
flowchart TB
accTitle: 进入MFT之前的确认
accDescr: 说明想把自定义的解码器或转换器插入管道时才进入MFT,但由于COM式的契约会更突出,应先确认Source Reader、Sink Writer、Media Session是否已经够用。
first["先看其他三个入口是否够用"] --> q{"是否需要自定义的转换部件"}
q -->|"否"| use3["用Reader、Writer或Session推进"]
q -->|"是"| mft["进入MFT(IMFTransform)"]
mft -.-> heavy["COM式的契约会更突出"]
图18:MFT 是最后的入口。不要一上来就进,先确认其他三个不够用再走。
6. 实务检查清单
最后,把实务中最先应该确认的要点整理成一张表。
| 项目 | 需要确认的内容 | 忽略后容易出现的问题 |
|---|---|---|
| 初始化职责 | 决定在哪里调用 CoInitializeEx 和 MFStartup,以及由谁负责终止处理 |
初始化遗漏、终止顺序混乱 |
| apartment | 提前决定使用 MF 的线程是 STA 还是 MTA | callback 相关的混乱、与 UI 冲突 |
| Source Reader 的模式 | 在创建时决定是同步还是异步 | ReadSample 出现意外阻塞,且之后无法切换 |
| media type negotiation | 枚举输出格式,并明确指定实际使用的格式。步骤见 3.3 的 4 步(用 GetNativeMediaType 枚举 → 确认 major type → SetCurrentMediaType → 用 GetCurrentMediaType 读取确定值) |
出现 MF_E_INVALIDMEDIATYPE,或得到与预期不同的格式 |
| 对象生命周期 | 明确 Release、Unlock、ShutdownObject 的职责 |
内存泄漏、缓冲区滞留、终止时状态不一致 |
| activation object | 区分枚举结果究竟是本体还是 IMFActivate |
误以为可以 QueryInterface 而失败 |
| topology | 掌握当前处理的是 partial topology 还是 full topology | 以为“应该会自动连接”而卡住 |
| 错误检查 | 每次都检查 HRESULT、stream flags、event |
忽略部分失败而未察觉 |
| UI 联动 | 不在 callback 中直接操作 UI,只把结果传回 UI 线程 | 挂起、竞争、难以排查的故障 |
其中优先级特别高的是以下 3 点。
- 不要选错最初的入口 API
- 先分清 Source Reader / Sink Writer / Media Session 中哪一个才是真正需要的
- 提前确定 apartment
- 如果要把 STA 的 UI 与 Media Foundation 的 work queue 混用,先确定好搭桥的方式
- 不要草率处理 media type negotiation
- 用“大概是这个格式吧”推进,之后会变得相当难以理清
- 具体步骤已整理在 3.3 的“media type negotiation 的步骤”中
flowchart TB
accTitle: 检查清单中优先级最高的3项
accDescr: 说明不要选错最初的入口API、提前确定apartment、不要草率处理media type negotiation这3项优先级特别高,做到这些就能避免后续实现的混乱。
c1["不要选错入口API"] --> ease["避免后续实现的混乱"]
c2["提前确定apartment"] --> ease
c3["不要草率协调格式"] --> ease
图19:在检查清单中,最先抓住这 3 项最见效。
7. 总结
使用 Media Foundation 时突然涌现大量 COM 相关话题,并不是偶然。
- Media Foundation 是一个媒体处理平台
- 其 source / transform / sink / activation / callback 等边界都用 COM 接口来表示
- 因此
IUnknown、HRESULT、GUID、apartment、callback 这些话题会自然出现 - 但 Media Foundation 的本体是拥有 Media Session 和 topology 的媒体管道,并不只是 COM 的翻版
在实务中,按下面的顺序思考会相当有条理。
- 首先分清究竟需要 Source Reader / Sink Writer / Media Session / MFT 中的哪一个
- 提前确定 apartment 和 callback 的方针
- 认真处理 media type negotiation 和对象生命周期
flowchart TB
accTitle: 实务中的思考顺序
accDescr: 说明先分清需要哪个入口,再确定apartment和callback的方针,最后认真处理media type negotiation和对象生命周期,这就是实务中的梳理顺序。
o1["分清需要哪个入口"] --> o2["确定apartment和callback的方针"]
o2 --> o3["认真处理格式协调与生命周期"]
图20:实务中的思考顺序。按入口选定、线程方针、格式与生命周期的顺序来决定。
不需要一开始就理解全部内容。 只要先把握住 “Media Foundation 是媒体处理平台,COM 深深嵌入在它的边界处” 这一点,阅读文档和代码都会容易得多。
8. 参考资料
- Media Foundation and COM - Microsoft Learn
- Overview of the Media Foundation Architecture - Microsoft Learn
- Initializing Media Foundation - Microsoft Learn
- Source Reader - Microsoft Learn
- Using the Source Reader to Process Media Data - Microsoft Learn
- Using the Source Reader in Asynchronous Mode - Microsoft Learn
- Sink Writer - Microsoft Learn
- Activation Objects - Microsoft Learn
- About Topologies - Microsoft Learn
- IMFAttributes interface - Microsoft Learn
- IMFMediaType interface - Microsoft Learn
- IMFTransform interface - Microsoft Learn
- MFTEnumEx function - Microsoft Learn
- IMFSourceReader::GetNativeMediaType - Microsoft Learn
- ComPtr Class (Microsoft::WRL) - Microsoft Learn
- 避免 COM 的 STA/MTA 导致挂起的基础知识 | KomuraSoft Blog
- 从 C# 调用 C++ 原生 DLL 时,为什么最好用 C++/CLI 制作封装层 | KomuraSoft Blog
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
DLL・COM 接口的向后兼容性 ── 判断哪些改动会破坏调用方的对照表
DLL 或 COM 组件的哪些改动会破坏调用方?本文整理二进制兼容、源代码兼容、行为兼容这三层概念,给出按改动类型划分的判断表、COM 接口不可变的铁律,以及 semver 的实务运用方法,作为一份实务指南。
什么是 Reg-Free COM——无需注册使用 COM 的机制
梳理 Reg-Free COM 的基础、激活上下文与清单各自的作用、优点、局限,以及在实务中判断是否采用的依据。
用 Media Foundation 把图片和文字烧录进 MP4 的方法
本文整理用 Media Foundation 向 MP4 视频的每一帧烧录图片与文字、生成新 MP4 的思路,梳理 Source Reader、绘制、色彩转换、Sink Writer 各自的职责分工,并给出单文件即可运行的 C++ 示例。
在 Media Foundation 中把 YUV 转换为 RGB 的方法
从 Source Reader 自动转换与 NV12/YUY2 手动转换、stride、色彩空间几个角度,梳理在 Media Foundation 中把 YUV 帧转换为 RGB 的方法。
用 Media Foundation 从 MP4 的指定时刻提取静止图像的方法
本文整理用 Source Reader 从 MP4 中取出最接近指定时刻的帧,处理好 stride 与 RGB32 的第 4 个字节(alpha)之后保存为 PNG 的完整实现步骤。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
ActiveX 迁移
整理保留、包装或替换 COM / ActiveX / OCX 资产的阶段性判断的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
包含 Media Foundation、COM、HRESULT 的 Windows 媒体处理,接近作为 Windows 应用开发 来处理的实现主题。
技术咨询 & 设计评审
如果想先梳理清楚 COM 式的边界和初始化顺序,可以作为技术咨询与设计评审,从设计层面切入。
常见问题
汇总了咨询这一主题时常见的问题。
- Media Foundation 是什么?和 COM 有什么区别?
- Media Foundation 是 Windows 上用于处理视频和音频的媒体处理平台,并不是说整个 API 都是纯粹的 COM。不过,source / transform / sink / activation / attributes / callback 等部件之间的边界都是通过 COM 接口来表示的,所以使用过程中自然会涉及 IUnknown、HRESULT、GUID、apartment 这些话题。准确的理解是:“Media Foundation 是一个媒体处理平台,其边界处深深地嵌入了 COM”。
- 为什么 MFStartup 和 CoInitializeEx 两个都需要调用?
- 因为它们的职责不同。CoInitializeEx 用于初始化 COM 库,MFStartup 用于初始化 Media Foundation 平台。仅完成 COM 初始化是不够的,还需要单独初始化 Media Foundation。在实务中,提前决定好由哪个线程使用 Media Foundation、该线程应设为 STA 还是 MTA、由谁负责 MFStartup / MFShutdown 与 CoInitializeEx / CoUninitialize 的调用职责,之后处理回调和 UI 联动时会更轻松。
- Source Reader、Sink Writer、Media Session、MFT 该如何区分使用?
- 如果想从文件或摄像头中取出帧或采样数据,入口是 Source Reader;如果想把生成的音频、视频写入文件,入口是 Sink Writer。如果想把播放、停止、跳转、A/V 同步以及质量控制都交给平台处理,就要以 Media Session 为核心来考虑。如果想把自定义的解码器或转换器接入管道,就需要用到 MFT,但由于这里 COM 式的契约会更突出,建议先确认前面三者中哪一个才是真正需要的。
- 使用 Media Foundation 的异步回调时需要注意什么?
- Media Foundation 的异步处理使用 work queue,该线程是 MTA,因此把应用侧也统一到 MTA 会让实现更简单。IMFSourceReaderCallback 的实现必须是线程安全的,重要的是不要在回调中直接操作 UI 线程上的 STA 对象。如果需要更新 UI,应该只把结果传回 UI 线程。另外还要注意,Source Reader 的同步 / 异步模式在创建时就已确定,之后无法切换。