Media Foundation 是什麼 - COM 和 Windows 媒體 API 的臉浮現的原因

· · Media Foundation, COM, C++, Windows 開發

開始碰 Media Foundation 容易覺得「應該是在使用 Windows 的影像或音訊 API,COM 的話題卻突然增加」。 特別是 CoInitializeExHRESULTIMFSourceReaderIMFTransform 這一帶一起出現,Media Foundation 是什麼也變得難以看見。

CoInitializeExMFStartupIMFSourceReaderIMFMediaTypeIMFTransformIMFActivateHRESULT、GUID 等一口氣出現,空氣突然變成 Win32 / COM 式。

本文不是像辭典一樣網羅 Media Foundation 整體,而是先整理以下 3 個。

  • 為什麼使用 Media Foundation 時 COM 的話題自然浮現
  • 哪裡 COM 的顏色變濃
  • 最初該從 Source Reader / Sink Writer / Media Session / MFT 的哪裡開始碰

程式碼範例以 C++ 為基礎,但思考方式本身即使從 .NET 等透過封裝層碰也幾乎相同。

1. 先講結論(一句話)

  • Media Foundation 是處理影像和音訊的平台,API 整體並非直接就是純粹的 COM
  • 但是,source / transform / sink / activation / attributes / callback 的邊界以 COM 介面表現,所以使用時自然會出現 IUnknownHRESULT、GUID、apartment 的話題
  • 最初從 Source Reader / Sink Writer 入門,需要播放控制時進入 Media Session,需要自訂轉換器時進入 MFT,比較容易整理

簡言之,Media Foundation 是媒體處理的平台,其邊界面深入導入了 COM

先掌握這點,「為什麼突然變成 COM 的臉」就會相當容易看見。

2. 先看的整理表

2.1. 想做什麼時碰什麼

先看這個表,容易選擇入口。

想做的事 先碰的 COM 的濃度 補充
想從檔案或相機取得 frame / sample Source Reader 必要時也會照顧 decoder
想把生成的音訊 / 影像寫到檔案 Sink Writer 必要時可以一起處理 encoder 和 media sink
想處理播放、停止、seek、A/V 同步、品質控制 Media Session 需要理解 topology 和 session
想插入自訂的轉換器或 codec 式部件 MFT IMFTransform 為中心思考
想看了列舉的候選後,只實體化必要的 IMFActivate 回傳的不是本體而是 activation object 的情況

2.2. 哪裡會變成 COM 的臉

地點 出現什麼 先要理解的
初始化 CoInitializeEx, MFStartup COM 初始化和 Media Foundation 初始化是不同的
物件的建立・傳遞 IMFSourceReader, IMFMediaType, IMFTransform 大多是介面指標 + HRESULT
設定 IMFAttributes, GUID 設定值或型別資訊用 key/value + GUID 表現
列舉・延遲建立 IMFActivate, ActivateObject 列舉結果可能不是直接本體
非同步 IMFSourceReaderCallback, work queue 需要意識 callback 和 apartment
播放控制 topology, Media Session 整個管線的流程是 Media Foundation 固有的概念

2.3. 先掌握意思的用語

用語 這裡的意思
Media Source 把媒體資料放入管線的入口。檔案、網路、擷取裝置等
MFT Media Foundation Transform。解碼器、編碼器、影像轉換器等的共通模型
Media Sink 媒體資料的去處。畫面顯示、音訊輸出、寫入檔案等
Media Session 管理整個管線流程的機制。負責播放或同步
Topology 表示如何連接 source / transform / sink 的連接圖
Activation Object 為了之後建立本體的輔助物件。以 IMFActivate 表現
Attributes 以 GUID 為鍵的 key/value 儲存。Media Foundation 整體多用

先把這一帶當作用語持有,讀文件時的卡住會相當減少。

3. 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)"]
        App["應用程式"] --> Writer["Sink Writer (+ encoder)"]
        Writer --> Sink2["Media Sink"]
    end

Media Foundation 有大致以下 2 種使用方式。

  • 使用整個管線的模型
    • 連接 source / transform / sink,Media Session 管理資料流或 A/V 同步
  • 應用程式直接處理資料的模型
    • 用 Source Reader 從 source 取出資料,用 Sink Writer 流入 sink

後者在想自己處理 frame 或 sample 的場面比較容易進入。 另一方面,連播放或同步都想交給平台時前者是本道。

這裡重要的是 Media Foundation 的真面目是媒體處理的平台,與直接碰 COM 物件集合的感覺稍微不同

但是,開始看那些部件之間的邊界,COM 的臉會突然變濃。下面整理那裡。

4. Media Foundation 變成 COM 臉的地點

4.1. 初始化中 CoInitializeExMFStartup 並排

最初很多人會有違和感的是這裡。

在想開檔案、想從相機取得這些話題之前,先出現 CoInitializeExMFStartup

  • CoInitializeEx 是 COM 函式庫的初始化
  • MFStartup 是 Media Foundation 平台的初始化

也就是說,光是 COM 初始化還不夠,也需要 Media Foundation 側的初始化。 這裡就會知道「這不是只是影像 API,下面有相當多 COM 基礎的合約」。

實務上,在這時點決定以下後面比較輕鬆。

  • 哪個執行緒使用 Media Foundation
  • 把那個執行緒做成 STA 還是 MTA
  • MFStartup / MFShutdownCoInitializeEx / CoUninitialize 的職責由誰負責

讓這個設計模糊就進行的話,之後在 callback 或 UI 連動的地方會變得難以理解。

4.2. 物件的傳遞以介面為中心

讀 Media Foundation 的 API,回傳值或 out 參數的多數是 COM 介面。

  • IMFSourceReader
  • IMFMediaType
  • IMFTransform
  • IMFActivate
  • IMFSample
  • IMFMediaBuffer

這裡重要的是 不只是資料本體,連型別資訊或設定物件都以介面表現

例如,

  • IMFTransform 是表示 MFT 的介面
  • IMFAttributes 是 key/value 儲存
  • IMFMediaType 是繼承 IMFAttributes 的「媒體格式的說明」

也就是,連 media type 這樣「像是設定資料的東西」都以 COM 介面持有。 這裡自然會進入 IUnknownQueryInterfaceAddRef / ReleaseHRESULT 的脈絡。

flowchart TD
    IUnknown["IUnknown"]
    IUnknown --> IMFAttributes["IMFAttributes"]
    IMFAttributes --> IMFMediaType["IMFMediaType"]
    IMFAttributes --> IMFActivate["IMFActivate"]
    IUnknown --> IMFSourceReader["IMFSourceReader"]
    IUnknown --> IMFTransform["IMFTransform"]

到這裡就能看見「Media Foundation 是媒體 API,但邊界的表現方式相當 COM」。

4.3. 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 物件

這個形式與 Media Foundation 是 事後找可替換的部件組合的設計 的相性好。

此外,activation object 本身能持有 attributes,所以容易變成「先看候選的屬性」「必要時設定」「之後實體化」的流程。這裡也相當 COM 式。

4.4. 設定或型別資訊以 IMFAttributes 和 GUID 為中心

碰 Media Foundation,有個設定看起來突然充滿 GUID 的地點。 其中心是 IMFAttributes

IMFAttributes 是以 GUID 為鍵的 key/value 儲存。這個在 Media Foundation 整體被大量使用。

特別重要的是 IMFMediaTypeIMFMediaType 繼承 IMFAttributes,把媒體格式的資訊當作屬性持有。

例如以下資訊。

  • major type(音訊還是影像)
  • subtype(H.264、AAC、RGB32、PCM 等)
  • frame 尺寸
  • frame rate
  • sample rate
  • channel 數
flowchart LR
    MediaType["IMFMediaType"] --> Major["MF_MT_MAJOR_TYPE"]
    MediaType --> Subtype["MF_MT_SUBTYPE"]
    MediaType --> Detail["尺寸 / FPS / sample rate 等"]

這裡容易覺得是「GUID 的森林」,但實際上做的事相當自然。

  • 使用屬性儲存持有設定
  • media type 也作為屬性儲存表現
  • source / transform / sink 之間看那個屬性對格式進行協調

簡言之,設定和型別資訊的表現使用 COM 式的介面和 GUID

4.5. 非同步・回呼・執行緒的處理也是 COM 式

Media Foundation 的實務中容易忽略的是非同步處理和執行緒模型。

例如 Source Reader 預設是同步模式。同步模式中 ReadSample 阻塞。 依檔案、網路、裝置的狀態,那個等待可能會變成看得見的時間。

想做成非同步模式時,建立 Source Reader 時傳入 callback。 準備實作 IMFSourceReaderCallback 的物件,設定到 MF_SOURCE_READER_ASYNC_CALLBACK 屬性後再建立的流程。

再稍微重要的是 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(...)

這裡重要的是以下。

  • 不要直接在 callback 側碰 UI 執行緒的 STA 物件
  • callback 實作做成執行緒安全
  • 需要 UI 更新時,只把結果送回 UI 執行緒
  • 「Media Foundation 的 callback 從哪個執行緒來」最初固定來思考

Media Foundation 不會擅自吸收 STA 物件的狀況。 所以,把使用 Media Foundation 的 worker 靠向 MTA,與 UI 明確搭橋 比較容易整理。

4.6. 但是 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"]

也就是,Media Foundation 是 使用 COM 表現部件的合約,在其上作為媒體處理平台運作 的東西。

用這個 2 段構成看就相當容易理解。

5. 大致的區分使用

決定最初的入口時,以下圖通常就夠。

flowchart TD
    Start["想做的事"] --> Q1{"最初需要的是?"}
    Q1 -- "想讀 frame / sample" --> A1["Source Reader"]
    Q1 -- "想寫到檔案" --> A2["Sink Writer"]
    Q1 -- "需要播放控制或 A/V 同步" --> A3["Media Session"]
    Q1 -- "想放入自訂轉換器" --> A4["MFT"]

5.1. 先從 Source Reader 入門的情況

Source Reader 作為想從檔案或裝置取出資料時的入口相當好用。

適合的例如以下情況。

  • 想從影像檔取得 frame
  • 想解碼音訊檔取得 sample
  • 想從相機取得 frame
  • 想把 Media Foundation 的 source 連接到自己的處理管線

Source Reader 會依需要讀取 decoder,把資料交給應用程式。 另一方面,presentation clock 的管理、A/V 同步、畫面繪製本身不照顧。

也就是說,看作 不是「播放」而是「取得資料」的入口 比較容易理解。

5.2. 想寫入檔案就用 Sink Writer

Sink Writer 是想把音訊或影像寫到檔案時的入口。

適合的例如以下情況。

  • 想把生成的 frame 儲存為影像檔
  • 想編碼音訊 sample 並寫出
  • 想把讀出的資料轉換為其他格式儲存

Sink Writer 依需要找 encoder 讀取,管理到 media sink 的資料流。 與 Source 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 固有的概念也增加。

5.4. 插入自訂部件就用 MFT

MFT 是 Media Foundation 的 transform 的共通模型。

進入這裡的例如以下場面。

  • 想做自訂的解碼器或編碼器
  • 想把影像處理或音訊處理的部件插入管線
  • 想列舉 codec 或轉換器自行選擇
  • 想比預設的自動解析更深入控制

MFT 的世界中,IMFTransformIMFActivate、media type negotiation、sample / buffer 管理等 COM 式的合約相當浮到前面。 所以 比起作為最初入口直接進入 MFT,先看 Source Reader / Sink Writer / Media Session 哪個真的必要比較容易理解

6. 實務上的檢核表

最後把實務上最初該看的點整理成 1 頁。

項目 要看的 漏看容易發生的
初始化職責 決定在哪裡呼叫 CoInitializeExMFStartup、終止處理由誰持有 初始化遺漏、終止順序的混亂
apartment 先決定碰 MF 的執行緒做成 STA / MTA 的哪個 callback 周邊的混亂、與 UI 的衝突
Source Reader 的模式 建立時決定同步還是非同步 ReadSample 意外阻塞、事後無法切換
media type negotiation 列舉輸出格式,明確實際使用的格式 MF_E_INVALIDMEDIATYPE、來的是與期望不同的格式
物件壽命 明確 ReleaseUnlockShutdownObject 的職責 記憶體洩漏、buffer 保留、終止時的不一致
activation object 區別列舉結果是本體還是 IMFActivate 以為可以 QueryInterface 卻失敗
topology 把握處理的是 partial topology 還是 full topology 以為「應該自動連接」而卡住
錯誤確認 每次看 HRESULT、stream flags、event 一部分失敗卻沒察覺
UI 連動 不從 callback 直接碰 UI,只把結果送回 UI 執行緒 掛起、競爭、難以理解的缺陷

特別優先度高的是以下 3 個。

  1. 不要搞錯最初的入口 API
    • 先分開 Source Reader / Sink Writer / Media Session 哪個真的必要
  2. 先決定 apartment
    • 混合 STA 的 UI 和 Media Foundation 的 work queue 時,最初決定搭橋方式
  3. 不要草率處理 media type negotiation
    • 以「大概是這個格式吧」進行的話,之後會相當難以理解

7. 程式碼節錄

這裡不是完整範例,只放 能看出哪裡變成 COM 臉的節錄

7.1. 初始化

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();
}

這裡 CoInitializeExMFStartup 並排。 這是在碰 Media Foundation 時 COM 的空氣突然變濃的最初地點。

實作上,有時其他層已經負責 COM 初始化。那種情況也是,先固定由誰負責職責 比較安全。

7.2. 以同步模式建立 Source Reader

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,
        &timestamp,
        &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 frame」,在 Media Foundation 的邊界就會有相當 COM 式的臉。

7.3. 以非同步模式建立 Source Reader

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 放入屬性後建立 reader。

也就是,

  • callback 本身是 COM 介面
  • 非同步設定透過 IMFAttributes
  • 模式在建立時決定

這樣的形式。

實務上,把 IMFSourceReaderCallback 實作做成執行緒安全,不直接帶入 UI 物件很重要。

7.4. 用 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 臉」的感覺。

8. 總結

碰 Media Foundation 時 COM 的話題突然增加,不是偶然。

  • Media Foundation 是媒體處理的平台
  • 其 source / transform / sink / activation / callback 等的邊界以 COM 介面表現
  • 所以 IUnknownHRESULT、GUID、apartment、callback 的話題自然出現
  • 但是,Media Foundation 的本體是持有 Media Session 或 topology 的媒體管線,不是單純的 COM 重製

實務上,先以下面的順序思考就相當容易整理。

  1. 先分開究竟需要 Source Reader / Sink Writer / Media Session / MFT 的哪個
  2. 先決定 apartment 和 callback 的方針
  3. 仔細處理 media type negotiation 和物件壽命

不需要從一開始就試圖理解全部。 先看作 「Media Foundation 是媒體處理平台,COM 深深嵌入其邊界面」,文件和程式碼都會相當容易追。

9. 參考資料

相關文章

共用相同標籤的最新文章。能以相近的主題延伸理解。

相關主題

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

與本主題相關的服務

本文連結到以下服務頁面,歡迎從最接近的入口查看。

回到部落格一覽