Media Foundation 入門 - 從 COM 的視角理解這套 API
· 更新日期: · Go Komura · Media Foundation, COM, C++, Windows 開發
更新紀錄(3 筆,最後更新 2026年09月04日)
本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。
- 已將繁體中文版改寫為日文原文的完整翻譯。先前的繁體中文版只譯出日文原文的一部分,遺漏了章節、表格、Mermaid 圖、圖說與 FAQ。本次依日文原文將這些內容全部補回,並新增本文的知識地圖章節。技術主張與日文版一致。 查看更新前的版本 (DOI: 10.5281/zenodo.22279325)
- 補上了日文原文中已有的諮詢引導(consultation_services)。內文沒有改動。
- 修正了參考連結等處含有豎線(管線)符號的行被算繪為表格、導致連結無法點擊的顯示錯誤。內文內容未變動。
- 初次發布
引用本文(DOI: 10.5281/zenodo.21616245)
本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。
Go Komura(2026)。〈Media Foundation 入門 - 從 COM 的視角理解這套 API〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616245 https://comcomponent.com/zh-TW/blog/2026/03/09/002-media-foundation-why-it-feels-like-com/
- DOI(最新版本)
- 10.5281/zenodo.21616245
- DOI(此版本)
- 10.5281/zenodo.22297077
一開始接觸 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 為了跑非同步處理而具備的執行緒機制。回呼就是從這裡的執行緒被呼叫的(3.5) |
先把這些用語掌握起來,讀文件時卡住的地方會少掉相當多。
apartment 會在 3.5 成為主題,這裡只要先記得「Media Foundation 的回呼,可能來自和自己呼叫 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(+ 解碼器)"]
Reader --> App["應用程式"]
App --> Writer["Sink Writer(+ 編碼器)"]
Writer --> Sink2["Media Sink"]
end
圖 2: 兩種用法。把管線交給 Media Session 的模型,以及用 Reader / Writer 讓應用程式直接處理資料的模型。
Media Foundation 大致有以下 2 種用法。
- 使用整條管線的模型
- 把 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 |
必須留意回呼與 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 初始化。就算是那種情況,先把職責歸屬固定下來 還是比較安全。這個設計含糊不清就往下做,之後在回呼或 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)。檔案的話,一條串流通常只有一種類型,但網路攝影機會有多種格式 - 查看 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 的類別工廠會比較好懂。
在它登場的場合,列舉 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 的時候把回呼傳進去。
流程是準備一個實作了 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;
}
也就是說,
- 回呼本身是 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 的執行緒呼叫。
回呼這部分要注意的是以下幾點。
- 不要在回呼裡直接碰 UI 執行緒的 STA 物件
- 回呼的實作要做成執行緒安全
- 需要更新 UI 時,只把結果送回 UI 執行緒
- 一開始就把「Media Foundation 的回呼是從哪條執行緒來的」固定下來思考
Media Foundation 並不會自動替你處理好 STA 物件那邊的種種狀況。 因此,把使用 Media Foundation 的工作執行緒集中放在 MTA,再和 UI 之間明確地搭一座橋,會比較好梳理。
flowchart TB
accTitle: 回呼與 UI 執行緒之間的搭橋方式
accDescr: 說明回呼是從 MTA 的 work queue 執行緒過來的,所以實作要做成執行緒安全,不要直接碰 STA 的 UI 物件,需要更新 UI 時只把結果送回 UI 執行緒的梳理方式的圖。
cb["回呼來自 MTA 的 work queue"] --> safe["實作要做成執行緒安全"]
cb -.-> ng["不要直接碰 STA 的 UI 物件"]
safe --> bridge["只把結果送回 UI 執行緒"]
圖 11: 使用 Media Foundation 的工作執行緒集中放在 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 | 中 | 必要時連解碼器也會幫你處理好 |
| 想把產生出來的音訊 / 視訊寫出到檔案 | Sink Writer | 中 | 必要時可以把編碼器和 media sink 一起處理掉 |
| 想處理播放、停止、seek、A/V 同步乃至品質控制 | Media Session | 高 | 需要理解 topology 與 session |
| 想接進自訂的轉換器或 codec 式的元件 | MFT | 高 | 以 IMFTransform 為中心思考 |
| 想先看過列舉出來的候選,再只把需要的建立實體 | IMFActivate |
高 | 傳回來的有時不是本體,而是 activation object |
5.1. 先從 Source Reader 入門的情況
想從檔案或裝置取出資料時,Source Reader 是相當好用的入口。
適合的例如以下這些情況。
- 想從影片檔取得影格
- 想把音訊檔解碼後取得樣本
- 想從相機取得影格
- 想把 Media Foundation 的 source 接到自己的處理管線上
Source Reader 會依需要載入解碼器,把資料交給應用程式。 另一方面,presentation clock 的管理、A/V 同步,乃至畫面繪製本身,它都不管。
把它看成 不是為了「播放」,而是為了「取得資料」的入口,就容易理解了。
flowchart TB
accTitle: Source Reader 的職責範圍
accDescr: 說明 Source Reader 從檔案或相機取出資料,必要時載入解碼器再交給應用程式,但不負責 presentation clock 的管理、A/V 同步與畫面繪製這個職責範圍的圖。
src["檔案、相機等 source"] --> sr["Source Reader"]
sr --> app["把資料交給應用程式"]
sr -.-> dec["必要時載入解碼器"]
sr -.-> not["不管播放與同步"]
圖 15: Source Reader 不是為了「播放」,而是為了「取得資料」的入口。
5.2. 要寫出到檔案就用 Sink Writer
想把音訊或視訊寫出到檔案時,入口是 Sink Writer。
用途上,以下這些是典型。
- 想把產生出來的影格存成影片檔
- 想把音訊樣本編碼後寫出
- 想把讀出來的資料轉成其他格式再存起來
Sink Writer 會依需要找到並載入編碼器,管理往 media sink 的資料流。 它常常和 Source Reader 搭配使用,但兩者是各自獨立的元件,並不一定要成套使用。
flowchart TB
accTitle: Sink Writer 的職責範圍
accDescr: 說明應用程式產生的影格或音訊樣本交給 Sink Writer 之後,它會在必要時找到並載入編碼器,管理往 media sink 的資料流並寫出到檔案的圖。
app2["應用程式產生的影格、音訊"] --> sw["Sink Writer"]
sw -.-> enc["必要時載入編碼器"]
sw --> sink["寫出到 media sink"]
sw -.-> ind["與 Source Reader 各自獨立的元件"]
圖 16: Sink Writer 是負責編碼與寫出的入口。不一定要和 Reader 成套使用。
5.3. 連播放與同步都要處理就用 Media Session
如果要做的不是「想從檔案取得資料」,而是 想好好播放,那以 Media Session 為中心思考會比較自然。
出現以下這些需求時,就輪到 Media Session 上場。
- 想處理播放 / 停止 / seek
- 想把音訊與視訊的同步交給平台這一側
- 想連品質控制與格式變更一起納進管線來處理
- 想用 topology 組出 source / transform / sink 的流動
進到這一層,就比 Source Reader / Sink Writer 更靠近「Media Foundation 本體」。 相對地,topology 與 session event 這類 Media Foundation 特有的概念也會變多。
flowchart TB
accTitle: 選擇 Media Session 的判斷
accDescr: 說明如果連播放、停止、seek、A/V 同步與品質控制都想交給平台,就要以 Media Session 為中心思考,用 topology 組出流動,而 Media Foundation 特有的概念也會相應變多的圖。
need["連播放、seek、同步都想交出去"] --> 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["先看其他 3 個入口夠不夠用"] --> q{"需要自訂的轉換元件嗎"}
q -->|"不需要"| use3["用 Reader、Writer 或 Session 往下做"]
q -->|"需要"| mft["走進 MFT(IMFTransform)"]
mft -.-> heavy["COM 式的合約會浮上檯面"]
圖 18: MFT 是最後的入口。不要一開始就走進去,先確認其他 3 個都不夠用再說。
6. 實務上的檢核清單
最後,把實務上最先該看的幾點整理成一張表。
| 項目 | 要看的事 | 漏看容易發生的事 |
|---|---|---|
| 初始化職責 | 決定在哪裡呼叫 CoInitializeEx 與 MFStartup,以及結束處理由誰負責 |
漏掉初始化、結束順序混亂 |
| apartment | 先決定碰 MF 的執行緒要做成 STA 還是 MTA | 回呼這部分亂成一團、與 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 整合 | 不從回呼直接碰 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、回呼這些話題會自然冒出來 - 但 Media Foundation 的本體是持有 Media Session 與 topology 的媒體管線,並不是單純把 COM 重做一遍
實務上,先照以下順序思考,會相當好梳理。
- 先分辨究竟需要 Source Reader / Sink Writer / Media Session / MFT 當中的哪一個
- 先決定 apartment 與回呼的方針
- 仔細處理 media type negotiation 與物件生命週期
flowchart TB
accTitle: 實務上思考的順序
accDescr: 說明先分辨需要哪個入口,接著決定 apartment 與回呼的方針,最後仔細處理 media type negotiation 與物件生命週期,這是實務上梳理問題的順序的圖。
o1["分辨需要哪個入口"] --> o2["決定 apartment 與回呼的方針"]
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# 呼叫原生 DLL:C++/CLI 包裝層 vs P/Invoke | KomuraSoft Blog
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
用 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 的 alpha 整理好之後存成 PNG 的實作步驟。
Time Travel Debugging ── 把長期運轉中無法重現的問題「錄下來」再倒回去
一個月才出現一次的問題,當機傾印只拍得到結果。本文說明如何用 WinDbg 的 Time Travel Debugging(TTD) 錄下執行並倒回,涵蓋 TTD.exe 的錄製設計、環形緩衝區、TTD.Calls 查詢,以及和傾印的分工。
引數為什麼會壞掉 ── Windows 命令列引數的規則
在 Windows 上並不存在引數的陣列,傳給 CreateProcess 的是一條字串,分割由接收端負責。本文說明 CommandLineToArgvW・CRT・.NET 的分割規則,以及 .NET 的 ArgumentList 與 C++ 中正確的組裝方式。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
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 這些話題。把它看成「一個媒體處理平台,而 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。如果連播放、停止、seek、A/V 同步與品質控制都想交給平台,就以 Media Session 為中心來思考。想把自訂的解碼器或轉換器接進管線時才走到 MFT,但那裡 COM 式的合約會浮上檯面,所以建議先看清楚前面三個當中哪一個才是真正需要的。
- Media Foundation 的非同步回呼要注意什麼?
- Media Foundation 的非同步處理使用 work queue,而那些執行緒是 MTA,所以應用程式這一側也集中放在 MTA,實作會單純很多。重點是把 IMFSourceReaderCallback 的實作做成執行緒安全,而且不要在回呼裡直接碰 UI 執行緒的 STA 物件。需要更新 UI 時,只把結果送回 UI 執行緒。另外也要注意,Source Reader 的同步/非同步模式在建立時就決定了,事後無法切換。