用 Media Foundation 從 MP4 的指定時間擷取靜態影像的方法

· 更新日期: · · Media Foundation, C++, Windows 開發, WIC

更新紀錄(2 筆,最後更新 2026年09月04日)

本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。

已將繁體中文版改寫為日文原文的完整翻譯。先前的繁體中文版只譯出日文原文的一部分,遺漏了章節、表格、Mermaid 圖、圖說與 FAQ。本次依日文原文將這些內容全部補回,並新增本文的知識地圖章節。技術主張與日文版一致。 查看更新前的版本 (DOI: 10.5281/zenodo.22279392)
補上了日文原文中已有的諮詢引導(consultation_services)。內文沒有改動。 查看更新前的版本 (DOI: 10.5281/zenodo.21616278)
初次發布
引用本文(DOI: 10.5281/zenodo.21616277)

本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。

Go Komura(2026)。〈用 Media Foundation 從 MP4 的指定時間擷取靜態影像的方法〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616277 https://comcomponent.com/zh-TW/blog/2026/03/15/000-media-foundation-extract-still-image-from-mp4-at-specific-time/

DOI(最新版本)
10.5281/zenodo.21616277
DOI(此版本)
10.5281/zenodo.22297114

「想從 MP4 取出『第 12.3 秒的那一張』」這種需求其實相當常見。像是產生縮圖、檢驗日誌、監視錄影畫面的代表影格、設備日誌的佐證資料等等。

只是 Media Foundation 在這裡沒有那麼直接了當。看起來像是在 SetCurrentPosition 之後呼叫一次 ReadSample 就結束了,實際上卻牽涉 key frame、timestamp、stride、影像的上下方向、RGB32 的第 4 個位元組等等。做得草率一點,就會出現時間點稍微偏掉、影像上下顛倒、PNG 莫名其妙變透明這類不起眼卻很惱人的狀況。

草率處理時會出的狀況只是 seek 之後讀一次就儲存的實作,會牽涉 key frame 與 timestamp、stride、上下方向、RGB32 的第 4 個位元組,因而出現時間偏掉、上下顛倒、透明 PNG 等狀況的圖。seek 之後讀一次就儲存時間點稍微偏掉影像上下顛倒PNG 莫名變透明

圖 1: 乍看之下做到這樣就結束的實作,會導向時間偏掉、上下顛倒、透明 PNG 這些不起眼的狀況。

Media Foundation 的整體樣貌本身,可以參考之前寫的 Media Foundation 入門 - 從 COM 的角度理解 API。這次則再往下一層,只聚焦在 從 MP4 抽出一張 這件事。

本文會用 IMFSourceReader,把 從 MP4 取出最接近指定時間的靜態影像一張並存成 PNG 的做法,連同實務上容易踩到的陷阱一起梳理。而且在最後放了 可以直接貼進 Visual Studio C++ 主控台應用程式專案 .cpp 的單檔完整程式碼。文章中不會有切碎的片段程式碼,只要把最後那一整塊帶走就能動。

另外,本文出現的程式碼,已經以範例程式碼整套(單檔完成的 C++ 主控台應用程式)的形式在 GitHub 上公開。

media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)

1. 先講結論

先把結論整理出來,就是這樣。

  • 從 MP4 抽一張的話,這次用 Source Reader 當入口,比 Media Session 更直接了當
  • IMFSourceReader::SetCurrentPosition 不保證 exact seek。通常會落在 target 稍前,尤其偏向 key frame 那一側,所以之後必須用 ReadSample 往前推進,比較目標時間的前後
  • ReadSample 就算成功,也可能出現 pSample == nullptr。不能只看 HRESULT,還要看 flags 和 pSample
  • 把輸出的媒體類型設成 MFVideoFormat_RGB32,後面比較好儲存
  • 不過 RGB32 的第 4 個位元組不一定是 alpha,直接寫進 PNG 有時會變成透明影像。儲存前填入 0xFF 讓它不透明比較安全
  • 每列的 stride 和 top-down / bottom-up 如果隨手處理,影像就會亂掉,所以取出的樣本要先整成 top-down 的連續 BGRA,再交給 PNG

換句話說,seek -> 讀一次 -> 儲存 有點草率;做到 seek -> 看 timestamp 比較前後 -> 注意 stride 再複製 -> 儲存 PNG 這種程度,就會穩定不少。

草率的流程與穩定的流程seek 之後讀一次就儲存的流程很草率,改成 seek 之後一邊看 timestamp 一邊比較前後,注意 stride 複製之後再儲存 PNG 的流程就會穩定不少的圖。讀一次就儲存太草率seek一邊看 timestamp 一邊比較前後注意 stride 再複製儲存 PNG偏掉與亂掉的原因

圖 2: 不要在 seek 之後馬上儲存,中間夾進前後比較與注意 stride 的複製,就會穩定。

圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 15 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle

2. 本文的前提

2.1. 目標讀者與前置知識

  • 假設讀者在 C++ 裡呼叫過 Windows 的 COM API。只要理解「用 CoInitializeEx 初始化,再用 Release 釋放介面指標」這個流程就夠了
  • 傳回值的 HRESULT 用 SUCCEEDED / FAILED 巨集判斷。失敗時直接以 16 進位值來讀。開頭是 0x8007 的值來自 Win32 錯誤,低 16 bit 就是 Win32 的錯誤碼(0x80070057 與 E_INVALIDARG 是同一個值)。開頭是 MF_E_ 的 Media Foundation 專屬錯誤,定義在 mferror.h
  • 就算第一次接觸 Media Foundation 也讀得下去。從 COM 角度看到的整體樣貌,整理在 Media Foundation 入門 - 從 COM 的角度理解 API

2.2. 開發環境

項目 這次的前提
OS Windows 10 / Windows 11
IDE Visual Studio 2022(「使用 C++ 的桌面開發」工作負載)
SDK Visual Studio 隨附的 Windows SDK(包含 Media Foundation 與 WIC 的標頭檔和函式庫)
專案 C++ 的「主控台應用程式」範本
平台 x64
額外函式庫 無。mfplat.lib 這類連結指定,在程式碼裡用 #pragma comment(lib, ...) 就解決了

貼上程式碼的方式,以及先行編譯標頭檔周邊的細節注意事項,整理在「7. 建置與執行的筆記」。

2.3. 輸入與輸出的前提

這次的前提如下。

  • 輸入是本機的 MP4 檔案
  • 想要的是一張靜態影像
  • 傳回的不是「正好在指定時間」,而是「最接近指定時間的影格」
  • 實作採用同步模式的 IMFSourceReader
  • 儲存格式是用 WIC 寫出的 PNG
  • 不使用外部函式庫,只用 Windows 標準 API 完成
  • 以中途不會變更解析度的一般 MP4 為前提

如果要做到播放、音訊同步、seek bar、與 UI 連動,那會是另一種設計;但如果用途是 想取出一個影格,這樣做相當好懂。

這次前提的構圖以本機的 MP4 為輸入,用同步模式的 IMFSourceReader 取得最接近指定時間的影格,再用 WIC 存成 PNG,這種不需要外部函式庫的前提構成的圖。本機的 MP4同步模式的 IMFSourceReader最接近指定時間的影格用 WIC 儲存 PNG只用 Windows 標準 API 完成

圖 3: 從輸入到儲存,都用不需要外部函式庫的同步 Source Reader 構成走完。

3. 先看的整理表

3.1. 處理流程

要做的事 使用的 API 角色
開啟 MP4 MFCreateSourceReaderFromURL 從檔案建立媒體來源
只選影像 SetStreamSelection 避免讀取音訊
轉成 RGB32 SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING 取得方便儲存的未壓縮影格
移動到指定時間 SetCurrentPosition 以 100ns 為單位 seek
讀取影格 ReadSample 一張一張取得解碼完成的樣本
比較前一張與後一張 樣本的 timestamp 決定最接近指定時間的那一張
儲存成 PNG WIC 寫出成影像檔

3.2. 這次採用的判斷規則

雖然說是「指定時間的靜態影像」,但影片不是連續量,而是離散的影格,所以在實作上先決定要用哪條規則挑出一張,會比較輕鬆。

這次採用下面的規則。

  • seek 之後繼續呼叫 ReadSample
  • 保留 timestamp < target 的最後一個樣本
  • 當 timestamp >= target 的第一個樣本出現時,比較前一個樣本與目前樣本各自跟 target 的差
  • 採用比較接近 target 的那一個

這樣就比較容易取到 最接近 target 的那一張,而不是「target 之後的第一張」。

挑出最接近的一張的判斷規則seek 之後繼續呼叫 ReadSample,保留 target 之前的最後一個樣本,等 target 之後的第一個樣本出現時比較兩者的差,採用比較接近 target 的那一個的判斷規則的圖。seek 之後繼續呼叫 ReadSample保留 target 之前的最後一個樣本target 之後的第一個樣本出現比較兩者與 target 的差採用比較接近的那一個

圖 4: 先比較前後兩個候選再採用,取到的就不是「target 之後的第一張」,而是最接近的一張。

3.3. 處理的全貌

從處理的開頭到結尾,大致是 input.mp4 -> 建立 Source Reader -> 要求 RGB32 -> seek -> 反覆呼叫 ReadSample -> 比較 target 的前後 -> 重新排成 top-down BGRA -> 用 WIC 儲存 PNG。

看起來很單純,但 seek 的精度、樣本為 null、stride、第 4 個位元組的處理,各自都藏著小陷阱。只要不踩到這 4 個,實作本身會很順地收斂。

處理中潛藏的 4 個陷阱處理看起來單純,但有 seek 的精度、樣本為 null、stride、RGB32 的第 4 個位元組這 4 個小陷阱,只要不踩到就能讓實作順利收斂的圖。看起來單純的處理seek 的精度樣本為 nullstride 與方向第 4 個位元組的處理不踩到就能順利收斂

圖 5: 看似單純的流程各處藏著 4 個陷阱,只要留意這幾點,實作就能順利收斂。

4. 先掌握的陷阱

先把這一章開始出現的用語簡短整理一下。

用語 意思
key frame(關鍵影格) 不參考前後影格,單獨就能解碼的影格。在 H.264 中,IDR 圖像等屬於這一類
GOP(Group of Pictures) 從某個 key frame 到下一個 key frame 之前的一組影格。GOP 越長,key frame 的間隔就拉得越開,seek 之後的位置也越容易離指定時間很遠
stride 影像緩衝區中一列所佔的位元組數。不一定等於 寬度 × 每個像素的位元組數,每列結尾可能會有 padding
top-down / bottom-up 影像緩衝區的開頭是上緣那一列還是下緣那一列的差別。bottom-up 會以負的 stride 表示
MF_SOURCE_READERF_STREAMTICK ReadSample 傳回的 flag 之一,表示串流中有空隙(資料中斷)。帶著這個 flag 的呼叫拿不到影格,要重讀一次

4.1. SetCurrentPosition 不是 exact seek

如同 Microsoft Learn 的 IMFSourceReader::SetCurrentPosition 所寫,它不保證 exact seeking。影片通常會落在指定位置稍前,尤其偏向 key frame 那一側。而且它預設你之後會用 ReadSample 往前推進到目標位置。

因此,下面這種實作相當危險。

  • SetCurrentPosition(target)
  • 呼叫一次 ReadSample
  • 儲存那個影格

在 GOP 長的影片上,這樣就會偏掉。如果影片的 key frame 間隔拉開到 2 秒,就會存下比指定時間最多早了將近 2 秒的影格。

seek 之後只讀一次會偏掉的機制SetCurrentPosition 會落在指定位置稍前,尤其偏向 key frame 那一側,所以緊接著只讀一次就儲存時,在 GOP 長的影片上會存下比指定時間早很多的影格的圖。SetCurrentPosition(target)落在 key frame 那一側稍前的位置只呼叫一次 ReadSample結果存下較早的影格GOP 越長偏得越多

圖 6: seek 會偏向 key frame 那一側,只讀一次就可能存下早了整個 GOP 長度的畫面。

4.2. ReadSample 成功時也可能出現 pSample == nullptr

ReadSample 即使傳回 S_OK,ppSample 也可能是 NULL。到達尾端會傳回 MF_SOURCE_READERF_ENDOFSTREAM,遇到串流空隙會傳回 MF_SOURCE_READERF_STREAMTICK 等 flag。

只看 HRESULT 就直接取用 pSample 很危險。把 HRESULT、flags、pSample 湊成三件一組來看 才夠安全。

ReadSample 結果的三點確認ReadSample 即使傳回 S_OK 樣本也可能是 NULL,所以要把 HRESULT 與 flags 和 pSample 湊成三件一組確認,並處理尾端與串流空隙的 flag 的圖。ReadSample 的結果查看 HRESULT查看 flags查看 pSample可能出現尾端或空隙的 flag就算是 S_OK 也可能是 NULL

圖 7: 不要只憑成功代碼就取用,要把 HRESULT、flags、pSample 湊成三件一組確認。

4.3. 隨手處理 stride 與上下方向,影像就會亂掉

影像緩衝區不一定是以 width * bytesPerPixel 一路緊密排列。每列結尾可能會有 padding,RGB 系也可能是 bottom-up。Microsoft Learn 的 Image Stride 與 Uncompressed Video Buffers 也把這一點寫得相當清楚。

特別重要的是下面兩點。

  • IMF2DBuffer::Lock2D 會傳回 scan line 0 的起始指標 與 實際的 stride
  • 在 bottom-up 影像中,stride 有可能變成負值

這次採用 Microsoft Learn 的 helper 想法,最後重新排成 top-down 的連續 BGRA 緩衝區 之後再交給 PNG。先把這裡整理好,儲存端就會單純很多。

吸收 stride 與方向用 IMF2DBuffer 的 Lock2D 取得 scan line 0 的起始指標與實際的 stride,連 bottom-up 時 stride 為負的情況也一併重新排成 top-down 的連續 BGRA 緩衝區之後再交給 PNG 的流程的圖。用 Lock2D 取得scan line 0 的起始指標實際的 stridebottom-up 時可能是負值重新排成 top-down 的連續 BGRA儲存端變得單純

圖 8: 用 Lock2D 吸收實際的 stride 與方向,整成 top-down 的連續 BGRA 之後再交給儲存。

4.4. 不要把 MFVideoFormat_RGB32 的第 4 個位元組斷定成 alpha

MFVideoFormat_RGB32 和名字給人的感覺相反,並不是可以直接交給 PNG 的「乾淨 RGBA」。Windows 的 32bit RGB 中,byte 0、1、2 是 B、G、R,byte 3 可能是 alpha,也可能是 ignore。重點在於它不是 ARGB32。

如果把這裡當成 GUID_WICPixelFormat32bppBGRA 直接儲存,第 4 個位元組裡放的可能是 0,影像就會莫名變透明。這次採取的方針是 儲存前把 alpha 填成 0xFF,讓它完全不透明。

RGB32 第 4 個位元組的處理MFVideoFormat_RGB32 的 byte 0 到 2 是 B、G、R,第 4 個位元組不一定是 alpha,直接寫進 PNG 有時會變透明,所以採取儲存前把 alpha 填成 0xFF 讓它不透明的方針的圖。直接儲存填成 0xFF 再儲存MFVideoFormat_RGB32 的影格第 4 個位元組不一定是 alpha有時會變成透明 PNG完全不透明的 PNG

圖 9: 不要斷定第 4 個位元組的意思,儲存前填成 0xFF 固定為不透明。

5. 實作流程

5.1. 以同步模式建立 Source Reader

這次只要取到一張就好,所以不用非同步回呼,而用同步的 ReadSample。同步模式下 ReadSample 會阻塞到下一個樣本出現為止,但如果只是單次擷取靜態影像,實作會相當直接了當。

建立 Reader 時要做的是這 4 件事。

  • MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  • 先把所有 stream 關掉
  • 只開啟 MF_SOURCE_READER_FIRST_VIDEO_STREAM
  • 把輸出的 type 設成 MFMediaType_Video / MFVideoFormat_RGB32

這樣一來,後段就能以「收到 RGB32 的影格」為前提來寫。

建立 Reader 時的 4 個步驟啟用影像處理,先把所有 stream 關掉再只開啟第一個影像 stream,最後把輸出的 type 設成 RGB32,說明建立 Reader 時這 4 個步驟順序的圖。啟用影像處理關掉所有 stream只開啟第一個影像 stream把輸出的 type 設成 RGB32後段可以用 RGB32 為前提來寫

圖 10: 依序做完建立 Reader 的 4 個步驟,後段就只剩下接收 RGB32 的影格。

5.2. seek 之後一邊看 timestamp 一邊逼近

SetCurrentPosition 之後不要馬上儲存。用 ReadSample 一邊讀樣本,一邊比較 target 之前的最後一張,以及跨過 target 的第一張。

多做這一步,就能吸收掉相當多 seek 的粗糙。

seek 之後比較前後的對話應用程式用 SetCurrentPosition 做 seek 之後,反覆呼叫 ReadSample 查看 timestamp,等跨過 target 時比較前後的樣本並挑出比較接近的那一個的流程的圖。Source Reader應用程式Source Reader應用程式loop[直到跨過 target 為止]SetCurrentPosition(target)ReadSample樣本與 timestamp比較前後的樣本,採用比較接近的那一個

圖 11: 不要在 seek 之後馬上儲存,先讀到跨過 target 為止,再比較前後。

5.3. 把樣本整成 top-down BGRA

取出的樣本不直接寫進 PNG,先重新排進 top-down 的 BGRA 緩衝區。

  • 用 ConvertToContiguousBuffer 併成單一緩衝區
  • 用 BufferLock helper 取得 scan line 0 與 actual stride
  • 逐列複製到 top-down 緩衝區
  • 把 alpha 設成 0xFF

這樣儲存端就能把它當成「單純的 32bpp BGRA 影像」處理。

從樣本整成 BGRA 的步驟用 ConvertToContiguousBuffer 併成單一緩衝區,用 BufferLock 取得 scan line 0 與實際的 stride,逐列複製到 top-down 緩衝區再把 alpha 設成 0xFF 的整形步驟的圖。ConvertToContiguousBuffer用 BufferLock 取得 scan line 0 與 stride逐列複製到 top-down把 alpha 設成 0xFF單純的 32bpp BGRA 影像

圖 12: 經過這 4 個階段的整形,儲存端就能把它當成單純的 32bpp BGRA 影像處理。

5.4. PNG 儲存交給 WIC

儲存使用 WIC 的 IWICBitmapEncoder / IWICBitmapFrameEncode。分工是 Media Foundation 取得影格,WIC 產生影像。這一段只用 Windows 的標準 API 就能完成。

Media Foundation 與 WIC 的分工Media Foundation 負責取得影格,WIC 負責產生影像並寫出 PNG,說明兩者分工的圖。取得影格WIC 產生影像Media FoundationBGRA 影格PNG 檔案

圖 13: 影格取得交給 Media Foundation,產生影像交給 WIC,只用標準 API 就能完成。

6. 實務上的檢查清單

項目 要看的地方 漏掉之後容易發生的事
seek 精度 不要在 SetCurrentPosition 之後只讀一次就定案 存下比指定時間早很多的影格
樣本為 NULL HRESULT、flags、pSample 全部都看 在尾端或 stream tick 時對 null 解參考
stride 吸收 actual stride 與上下方向 影像亂掉、上下顛倒
RGB32 的第 4 個位元組 把 alpha 設成 0xFF 變成透明 PNG
時間範圍 遵守 0 <= target < duration 在尾端附近出現非預期的行為
連續擷取 不重建 Reader,反覆 seek 白白變慢
複製次數 大量處理時要留意 ConvertToContiguousBuffer 的成本 多耗掉 CPU 與記憶體頻寬
格式變化 中途會改變解析度的特殊影片要另外設計 寬度與高度的前提被打破

其中「連續擷取」與「複製次數」這兩列,不是抽一張本身的問題,而是擴大到從同一部影片抽出幾十張時才會顯現的項目。這次的範例是取一張就結束的構成,所以要抽多張時,請改成不重建 Source Reader,而是反覆呼叫 SetCurrentPosition 與 ReadSample。包含 Source Reader 以外的選項(Media Session 等)在內的 Media Foundation 全貌地圖,放在 Media Foundation 入門 - 從 COM 的角度理解 API。

從抽一張擴大到連續擷取的做法只抽一張的話維持這次的構成即可,要從同一部影片抽多張時就改成不重建 Source Reader 而反覆 seek 與 ReadSample 的擴大做法的圖。只抽一張抽多張要抽幾張維持這次的構成不重建 Reader,反覆 seek 與讀取也要留意複製次數的成本

圖 14: 擴大成抽多張時,要改成不重建 Reader,反覆做 seek 與讀取。

7. 建置與執行的筆記

本文最後的程式碼,做成了 容易以單一 .cpp 加進 Visual Studio C++ 主控台應用程式的形式。環境本身的前提整理在 2.2。

7.1. 建置的筆記

列出先掌握就會比較輕鬆的幾點。

  • 因為已經放了 #pragma comment(lib, ...),基本上不需要額外的連結器設定
  • 因為用的是 wmain,命令列引數可以維持 Unicode 直接處理
  • 為了讓預設的 Console App 範本裡有 pch.h 或 stdafx.h 時也好貼上,程式碼開頭用 __has_include 把它們撈進來
  • 即使如此,如果專案端強制使用自訂的先行編譯標頭檔,只要把這個 .cpp 設成「不使用先行編譯標頭檔」就能通過
  • 執行組態建議用 x64

7.2. 執行方式,以及成功時會出現什麼

用法是 ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>。例如像 ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png 這樣執行。

成功時,wmain 的最後會往標準輸出印出下面 3 行。

Saved: C:\work\frame.png
Requested: 12.345 sec
Actual: (採用的影格的顯示時間) sec

Requested 就是傳進引數的秒數本身,Actual 則是實際採用的影格的 timestamp。因為會依 3.2 的規則從前後兩張影格裡挑比較接近的那一張,所以兩者的差會落在影格間隔的大約一半以內(29.97fps 的話,大致的參考上限是 17ms 左右)。如果這裡差到數百 ms 以上,就要懷疑 seek 之後只讀了一次,也就是 4.1 的前後比較沒有生效。

Requested 與 Actual 差距的看法Requested 與 Actual 的差落在影格間隔一半以內就算正常,差到數百毫秒以上就要懷疑 seek 之後的前後比較沒有生效的診斷流程的圖。在影格間隔的一半以內數百 ms 以上查看 Requested 與 Actual 的差前後比較有生效懷疑前後比較沒有生效檢查是不是 seek 之後只讀了一次

圖 15: 差距如果大幅超過影格間隔的一半,就要懷疑前後比較的實作。

驗證動作的目標是這 3 點。

  • 結束代碼是 0,而且 Saved: 那一行印出指定的輸出路徑
  • 打開輸出的 PNG,該時間點的畫面會以正確方向(上下沒有顛倒)顯示
  • PNG 的寬度與高度和原影片的解析度一致,背景沒有透出來(4.4 的 alpha 填值有生效)

失敗時會往標準錯誤輸出印出 Failed. HRESULT = 0x........。這個值依 2.1 的要領來讀。指定的秒數大於等於影片長度時,會是 0x80070057(E_INVALIDARG)。

執行結果的確認流程執行成功就確認 3 行輸出與 PNG 的方向、解析度、不透明,失敗就讀標準錯誤輸出的 HRESULT,指定秒數大於等於影片長度時會是 E_INVALIDARG 的確認流程的圖。成功失敗執行確認 3 行輸出與 PNG讀標準錯誤的 HRESULT秒數大於等於影片長度就是 E_INVALIDARG

圖 16: 成功就確認輸出與 PNG 這 3 點,失敗就從 HRESULT 的值讀出原因。

8. 總結

用 Media Foundation 從 MP4 取出指定時間的靜態影像時,只盯著 SetCurrentPosition 和 ReadSample 是不夠的。實際上,

  • seek 不是 exact
  • 影格最好看著 timestamp 做前後比較
  • ReadSample 就算成功也可能沒有樣本
  • 吸收 stride 與影像方向之後再儲存
  • 不要把 RGB32 的第 4 個位元組斷定成 alpha

把這些都掌握住,就相當不容易出事。

要掌握的 5 點seek 不是 exact、用 timestamp 做前後比較、確認成功時也可能沒有樣本、吸收 stride 與方向、不要斷定 RGB32 的第 4 個位元組,掌握這 5 點就不容易出事的圖。seek 不是 exact用 timestamp 做前後比較確認成功時也可能沒有樣本吸收 stride 與方向不要斷定第 4 個位元組不容易出事的實作

圖 17: 這 5 個掌握點都到位,指定時間的靜態影像擷取就相當不容易出事。

這次的範例是把範圍縮到 好好抽出一張 的最小構成。像產生縮圖、儲存監視錄影畫面的代表影格、輸出檢驗日誌的佐證資料這些場合,都可以直接搬過去用。

9. 參考資料

10. 可直接貼進 .cpp 的完整程式碼

下面這一整塊,就是設定成直接帶進 Visual Studio C++ 主控台應用程式專案的程式碼。命令列引數的順序是 input.mp4、seconds、output.png。因為做成單檔完成的構成,所以很好貼進專案。

因為是很長的一整塊,先把哪個函式對應到本文哪一節放在前面。不論是閱讀時,還是動不起來要追查時,從這張對照表切入都比較快。

程式碼中的函式與類別 對應的本文 角色,以及處理的陷阱
MediaFoundationScope 5.1 把 CoInitializeEx 與 MFStartup 的初始化和收尾整合起來
CreateConfiguredSourceReader 5.1 建立 Reader,只選影像串流,並要求 MFVideoFormat_RGB32
GetPresentationDuration 6.(時間範圍) 取得影片長度,確認是否滿足 0 <= target < duration
SeekSourceReader 4.1 / 5.2 用 SetCurrentPosition 做 seek。光靠這裡不會是 exact
ReadNearestVideoSample 3.2 / 4.1 / 4.2 / 5.2 比較 target 的前後挑出一張。flags 與 pSample 為 null 也在這裡處理
GetDefaultStride 4.3 沒有 MF_MT_DEFAULT_STRIDE 時計算 stride 補上
BufferLock 4.3 / 5.3 用 IMF2DBuffer::Lock2D 取得 scan line 0 與實際的 stride
CopyContiguousBufferToTopDownBgra 4.3 / 4.4 / 5.3 逐列重新排進 top-down 緩衝區,並把第 4 個位元組填成 0xFF
CopySampleToTopDownBgra 5.3 取出 frame size 與 stride,再呼叫上面的複製
SaveBgraToPng 5.4 用 WIC 把 32bpp BGRA 寫出成 PNG
ExtractFrameFromMp4ToPng 5. 整體 依序呼叫上述函式的入口
TryParseSeconds / wmain 7.2 解析引數,並顯示 Requested / Actual
#define NOMINMAX
#if defined(_MSC_VER)
#  if __has_include("pch.h")
#    include "pch.h"
#  elif __has_include("stdafx.h")
#    include "stdafx.h"
#  endif
#endif
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <mfobjects.h>
#include <propvarutil.h>
#include <wincodec.h>

#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cwchar>
#include <cmath>
#include <cstring>
#include <limits>
#include <vector>

#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "propsys.lib")
#pragma comment(lib, "windowscodecs.lib")

template <class T>
void SafeRelease(T** pp)
{
    if (pp != nullptr && *pp != nullptr)
    {
        (*pp)->Release();
        *pp = nullptr;
    }
}

class MediaFoundationScope
{
public:
    MediaFoundationScope() : m_comInitialized(false), m_mfStarted(false)
    {
    }

    HRESULT Initialize()
    {
        HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
        if (hr == RPC_E_CHANGED_MODE)
        {
            return hr;
        }

        if (SUCCEEDED(hr))
        {
            m_comInitialized = true;
        }

        hr = MFStartup(MF_VERSION);
        if (FAILED(hr))
        {
            if (m_comInitialized)
            {
                CoUninitialize();
                m_comInitialized = false;
            }
            return hr;
        }

        m_mfStarted = true;
        return S_OK;
    }

    ~MediaFoundationScope()
    {
        if (m_mfStarted)
        {
            MFShutdown();
        }

        if (m_comInitialized)
        {
            CoUninitialize();
        }
    }

private:
    bool m_comInitialized;
    bool m_mfStarted;
};

HRESULT GetPresentationDuration(IMFSourceReader* pReader, LONGLONG* phnsDuration)
{
    if (pReader == nullptr || phnsDuration == nullptr)
    {
        return E_POINTER;
    }

    PROPVARIANT var;
    PropVariantInit(&var);

    HRESULT hr = pReader->GetPresentationAttribute(
        MF_SOURCE_READER_MEDIASOURCE,
        MF_PD_DURATION,
        &var);

    if (SUCCEEDED(hr))
    {
        hr = PropVariantToInt64(var, phnsDuration);
    }

    PropVariantClear(&var);
    return hr;
}

HRESULT GetDefaultStride(IMFMediaType* pType, LONG* plStride)
{
    if (pType == nullptr || plStride == nullptr)
    {
        return E_POINTER;
    }

    LONG lStride = 0;
    HRESULT hr = pType->GetUINT32(
        MF_MT_DEFAULT_STRIDE,
        reinterpret_cast<UINT32*>(&lStride));

    if (FAILED(hr))
    {
        GUID subtype = GUID_NULL;
        UINT32 width = 0;
        UINT32 height = 0;

        hr = pType->GetGUID(MF_MT_SUBTYPE, &subtype);
        if (FAILED(hr))
        {
            return hr;
        }

        hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &width, &height);
        if (FAILED(hr))
        {
            return hr;
        }

        hr = MFGetStrideForBitmapInfoHeader(subtype.Data1, width, &lStride);
        if (FAILED(hr))
        {
            return hr;
        }

        (void)pType->SetUINT32(MF_MT_DEFAULT_STRIDE, static_cast<UINT32>(lStride));
    }

    *plStride = lStride;
    return S_OK;
}

class BufferLock
{
public:
    explicit BufferLock(IMFMediaBuffer* pBuffer)
        : m_pBuffer(pBuffer),
          m_p2DBuffer(nullptr),
          m_locked(false)
    {
        if (m_pBuffer != nullptr)
        {
            m_pBuffer->AddRef();
            (void)m_pBuffer->QueryInterface(IID_PPV_ARGS(&m_p2DBuffer));
        }
    }

    ~BufferLock()
    {
        UnlockBuffer();
        SafeRelease(&m_p2DBuffer);
        SafeRelease(&m_pBuffer);
    }

    HRESULT LockBuffer(
        LONG defaultStride,
        DWORD heightInPixels,
        BYTE** ppScanLine0,
        LONG* plStride)
    {
        if (ppScanLine0 == nullptr || plStride == nullptr)
        {
            return E_POINTER;
        }

        *ppScanLine0 = nullptr;
        *plStride = 0;

        HRESULT hr = S_OK;

        if (m_p2DBuffer != nullptr)
        {
            hr = m_p2DBuffer->Lock2D(ppScanLine0, plStride);
        }
        else
        {
            BYTE* pData = nullptr;
            hr = m_pBuffer->Lock(&pData, nullptr, nullptr);
            if (SUCCEEDED(hr))
            {
                *plStride = defaultStride;

                if (defaultStride < 0)
                {
                    const size_t strideAbs = static_cast<size_t>(-defaultStride);
                    *ppScanLine0 = pData + strideAbs * (heightInPixels - 1);
                }
                else
                {
                    *ppScanLine0 = pData;
                }
            }
        }

        m_locked = SUCCEEDED(hr);
        return hr;
    }

    void UnlockBuffer()
    {
        if (!m_locked)
        {
            return;
        }

        if (m_p2DBuffer != nullptr)
        {
            (void)m_p2DBuffer->Unlock2D();
        }
        else if (m_pBuffer != nullptr)
        {
            (void)m_pBuffer->Unlock();
        }

        m_locked = false;
    }

private:
    IMFMediaBuffer* m_pBuffer;
    IMF2DBuffer* m_p2DBuffer;
    bool m_locked;
};

HRESULT CreateConfiguredSourceReader(PCWSTR inputPath, IMFSourceReader** ppReader)
{
    if (inputPath == nullptr || ppReader == nullptr)
    {
        return E_POINTER;
    }

    *ppReader = nullptr;

    IMFAttributes* pAttributes = nullptr;
    IMFSourceReader* pReader = nullptr;
    IMFMediaType* pRequestedType = nullptr;

    HRESULT hr = MFCreateAttributes(&pAttributes, 1);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = MFCreateSourceReaderFromURL(inputPath, pAttributes, &pReader);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = MFCreateMediaType(&pRequestedType);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pRequestedType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pRequestedType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pReader->SetCurrentMediaType(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        nullptr,
        pRequestedType);
    if (FAILED(hr))
    {
        goto done;
    }

    *ppReader = pReader;
    pReader = nullptr;

done:
    SafeRelease(&pRequestedType);
    SafeRelease(&pReader);
    SafeRelease(&pAttributes);
    return hr;
}

HRESULT SeekSourceReader(IMFSourceReader* pReader, LONGLONG targetHns)
{
    if (pReader == nullptr)
    {
        return E_POINTER;
    }

    PROPVARIANT var;
    PropVariantInit(&var);

    HRESULT hr = InitPropVariantFromInt64(targetHns, &var);
    if (SUCCEEDED(hr))
    {
        hr = pReader->SetCurrentPosition(GUID_NULL, var);
    }

    PropVariantClear(&var);
    return hr;
}

HRESULT ReadNearestVideoSample(
    IMFSourceReader* pReader,
    LONGLONG targetHns,
    IMFSample** ppSample,
    LONGLONG* pChosenTimestampHns)
{
    if (pReader == nullptr || ppSample == nullptr)
    {
        return E_POINTER;
    }

    *ppSample = nullptr;
    if (pChosenTimestampHns != nullptr)
    {
        *pChosenTimestampHns = 0;
    }

    IMFSample* pBefore = nullptr;
    LONGLONG beforeTimestamp = 0;
    bool hasBefore = false;

    HRESULT hr = S_OK;

    for (;;)
    {
        IMFSample* pCurrent = nullptr;
        DWORD flags = 0;
        LONGLONG currentTimestamp = 0;
        LONGLONG diffBefore = 0;
        LONGLONG diffCurrent = 0;

        hr = pReader->ReadSample(
            MF_SOURCE_READER_FIRST_VIDEO_STREAM,
            0,
            nullptr,
            &flags,
            &currentTimestamp,
            &pCurrent);

        if (FAILED(hr))
        {
            SafeRelease(&pCurrent);
            break;
        }

        if ((flags & MF_SOURCE_READERF_ENDOFSTREAM) != 0)
        {
            SafeRelease(&pCurrent);

            if (hasBefore)
            {
                *ppSample = pBefore;
                pBefore = nullptr;

                if (pChosenTimestampHns != nullptr)
                {
                    *pChosenTimestampHns = beforeTimestamp;
                }

                hr = S_OK;
            }
            else
            {
                hr = MF_E_END_OF_STREAM;
            }
            break;
        }

        if ((flags & MF_SOURCE_READERF_STREAMTICK) != 0)
        {
            SafeRelease(&pCurrent);
            continue;
        }

        if (pCurrent == nullptr)
        {
            continue;
        }

        if (currentTimestamp < targetHns)
        {
            SafeRelease(&pBefore);
            pBefore = pCurrent;
            pCurrent = nullptr;
            beforeTimestamp = currentTimestamp;
            hasBefore = true;
            continue;
        }

        if (hasBefore)
        {
            diffBefore = targetHns - beforeTimestamp;
            diffCurrent = currentTimestamp - targetHns;

            if (diffBefore <= diffCurrent)
            {
                *ppSample = pBefore;
                pBefore = nullptr;

                if (pChosenTimestampHns != nullptr)
                {
                    *pChosenTimestampHns = beforeTimestamp;
                }

                SafeRelease(&pCurrent);
            }
            else
            {
                *ppSample = pCurrent;
                pCurrent = nullptr;

                if (pChosenTimestampHns != nullptr)
                {
                    *pChosenTimestampHns = currentTimestamp;
                }
            }
        }
        else
        {
            *ppSample = pCurrent;
            pCurrent = nullptr;

            if (pChosenTimestampHns != nullptr)
            {
                *pChosenTimestampHns = currentTimestamp;
            }
        }

        hr = S_OK;
        break;
    }

    SafeRelease(&pBefore);
    return hr;
}

HRESULT CopyContiguousBufferToTopDownBgra(
    IMFMediaBuffer* pBuffer,
    LONG defaultStride,
    UINT32 width,
    UINT32 height,
    std::vector<BYTE>& pixels,
    UINT32* pStride)
{
    if (pBuffer == nullptr || pStride == nullptr)
    {
        return E_POINTER;
    }

    BufferLock lock(pBuffer);

    BYTE* pScanLine0 = nullptr;
    LONG actualStride = 0;

    HRESULT hr = lock.LockBuffer(defaultStride, height, &pScanLine0, &actualStride);
    if (FAILED(hr))
    {
        return hr;
    }

    if (width > (std::numeric_limits<UINT32>::max() / 4))
    {
        return E_INVALIDARG;
    }

    const UINT32 destStride = width * 4;
    const LONG actualStrideAbs = (actualStride < 0) ? -actualStride : actualStride;
    if (actualStrideAbs < static_cast<LONG>(destStride))
    {
        return E_UNEXPECTED;
    }

    pixels.resize(static_cast<size_t>(destStride) * height);

    BYTE* pDestRow = pixels.data();
    BYTE* pSrcRow = pScanLine0;

    for (UINT32 y = 0; y < height; ++y)
    {
        std::memcpy(pDestRow, pSrcRow, destStride);

        // MFVideoFormat_RGB32 的第 4 個 byte 不一定是 alpha,
        // 所以在儲存成 PNG 之前固定為不透明。
        for (UINT32 x = 0; x < width; ++x)
        {
            pDestRow[static_cast<size_t>(x) * 4 + 3] = 0xFF;
        }

        pDestRow += destStride;
        pSrcRow += actualStride;
    }

    *pStride = destStride;
    return S_OK;
}

HRESULT CopySampleToTopDownBgra(
    IMFSample* pSample,
    IMFMediaType* pCurrentType,
    std::vector<BYTE>& pixels,
    UINT32* pWidth,
    UINT32* pHeight,
    UINT32* pStride)
{
    if (pSample == nullptr || pCurrentType == nullptr ||
        pWidth == nullptr || pHeight == nullptr || pStride == nullptr)
    {
        return E_POINTER;
    }

    *pWidth = 0;
    *pHeight = 0;
    *pStride = 0;

    IMFMediaBuffer* pBuffer = nullptr;

    GUID subtype = GUID_NULL;
    UINT32 width = 0;
    UINT32 height = 0;
    LONG defaultStride = 0;

    HRESULT hr = pCurrentType->GetGUID(MF_MT_SUBTYPE, &subtype);
    if (FAILED(hr))
    {
        goto done;
    }

    if (!IsEqualGUID(subtype, MFVideoFormat_RGB32))
    {
        hr = MF_E_INVALIDMEDIATYPE;
        goto done;
    }

    hr = MFGetAttributeSize(pCurrentType, MF_MT_FRAME_SIZE, &width, &height);
    if (FAILED(hr))
    {
        goto done;
    }

    if (width == 0 || height == 0)
    {
        hr = E_UNEXPECTED;
        goto done;
    }

    hr = GetDefaultStride(pCurrentType, &defaultStride);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pSample->ConvertToContiguousBuffer(&pBuffer);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = CopyContiguousBufferToTopDownBgra(
        pBuffer,
        defaultStride,
        width,
        height,
        pixels,
        pStride);
    if (FAILED(hr))
    {
        goto done;
    }

    *pWidth = width;
    *pHeight = height;

    hr = S_OK;

done:
    SafeRelease(&pBuffer);
    return hr;
}

HRESULT SaveBgraToPng(
    PCWSTR outputPath,
    const BYTE* pixels,
    UINT32 width,
    UINT32 height,
    UINT32 stride)
{
    if (outputPath == nullptr || pixels == nullptr)
    {
        return E_POINTER;
    }

    if (width == 0 || height == 0 || stride < width * 4)
    {
        return E_INVALIDARG;
    }

    const size_t bufferSizeSizeT = static_cast<size_t>(stride) * height;
    if (bufferSizeSizeT > static_cast<size_t>(std::numeric_limits<UINT>::max()))
    {
        return E_INVALIDARG;
    }

    const UINT bufferSize = static_cast<UINT>(bufferSizeSizeT);

    IWICImagingFactory* pFactory = nullptr;
    IWICStream* pStream = nullptr;
    IWICBitmapEncoder* pEncoder = nullptr;
    IWICBitmapFrameEncode* pFrame = nullptr;
    IPropertyBag2* pProps = nullptr;
    WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppBGRA;

    HRESULT hr = CoCreateInstance(
        CLSID_WICImagingFactory,
        nullptr,
        CLSCTX_INPROC_SERVER,
        IID_PPV_ARGS(&pFactory));
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFactory->CreateStream(&pStream);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pStream->InitializeFromFilename(outputPath, GENERIC_WRITE);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &pEncoder);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->Initialize(pStream, WICBitmapEncoderNoCache);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->CreateNewFrame(&pFrame, &pProps);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFrame->Initialize(pProps);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFrame->SetSize(width, height);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFrame->SetPixelFormat(&pixelFormat);
    if (FAILED(hr))
    {
        goto done;
    }

    if (!IsEqualGUID(pixelFormat, GUID_WICPixelFormat32bppBGRA))
    {
        hr = WINCODEC_ERR_UNSUPPORTEDPIXELFORMAT;
        goto done;
    }

    hr = pFrame->WritePixels(
        height,
        stride,
        bufferSize,
        const_cast<BYTE*>(pixels));
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFrame->Commit();
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->Commit();

done:
    SafeRelease(&pProps);
    SafeRelease(&pFrame);
    SafeRelease(&pEncoder);
    SafeRelease(&pStream);
    SafeRelease(&pFactory);
    return hr;
}

HRESULT ExtractFrameFromMp4ToPng(
    PCWSTR inputPath,
    LONGLONG targetHns,
    PCWSTR outputPath,
    LONGLONG* pActualTimestampHns)
{
    if (inputPath == nullptr || outputPath == nullptr)
    {
        return E_POINTER;
    }

    if (targetHns < 0)
    {
        return E_INVALIDARG;
    }

    MediaFoundationScope mf;
    HRESULT hr = mf.Initialize();
    if (FAILED(hr))
    {
        return hr;
    }

    IMFSourceReader* pReader = nullptr;
    IMFMediaType* pCurrentType = nullptr;
    IMFSample* pChosenSample = nullptr;

    LONGLONG durationHns = 0;
    UINT32 width = 0;
    UINT32 height = 0;
    UINT32 stride = 0;
    std::vector<BYTE> pixels;

    hr = CreateConfiguredSourceReader(inputPath, &pReader);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pReader->GetCurrentMediaType(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        &pCurrentType);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = GetPresentationDuration(pReader, &durationHns);
    if (FAILED(hr))
    {
        goto done;
    }

    if (targetHns >= durationHns)
    {
        hr = E_INVALIDARG;
        goto done;
    }

    hr = SeekSourceReader(pReader, targetHns);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = ReadNearestVideoSample(
        pReader,
        targetHns,
        &pChosenSample,
        pActualTimestampHns);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = CopySampleToTopDownBgra(
        pChosenSample,
        pCurrentType,
        pixels,
        &width,
        &height,
        &stride);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = SaveBgraToPng(outputPath, pixels.data(), width, height, stride);

done:
    SafeRelease(&pChosenSample);
    SafeRelease(&pCurrentType);
    SafeRelease(&pReader);
    return hr;
}

bool TryParseSeconds(PCWSTR text, LONGLONG* phns)
{
    if (text == nullptr || phns == nullptr)
    {
        return false;
    }

    wchar_t* end = nullptr;
    errno = 0;

    const double seconds = std::wcstod(text, &end);
    if (end == text || *end != L'\0' || errno != 0)
    {
        return false;
    }

    if (!std::isfinite(seconds) || seconds < 0.0)
    {
        return false;
    }

    const long double hns =
        static_cast<long double>(seconds) * 10000000.0L;

    if (hns < 0.0L ||
        hns > static_cast<long double>(std::numeric_limits<LONGLONG>::max()))
    {
        return false;
    }

    *phns = static_cast<LONGLONG>(std::llround(hns));
    return true;
}

double HnsToSeconds(LONGLONG hns)
{
    return static_cast<double>(hns) / 10000000.0;
}

void PrintUsage()
{
    std::fwprintf(stderr, L"Usage:\n");
    std::fwprintf(stderr, L"  ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>\n");
    std::fwprintf(stderr, L"\nExample:\n");
    std::fwprintf(stderr, L"  ExtractFrameFromMp4.exe input.mp4 12.345 output.png\n");
}

int wmain(int argc, wchar_t* argv[])
{
    if (argc != 4)
    {
        PrintUsage();
        return 1;
    }

    LONGLONG targetHns = 0;
    if (!TryParseSeconds(argv[2], &targetHns))
    {
        std::fwprintf(stderr, L"Invalid seconds: %ls\n", argv[2]);
        return 1;
    }

    LONGLONG actualHns = 0;
    HRESULT hr = ExtractFrameFromMp4ToPng(
        argv[1],
        targetHns,
        argv[3],
        &actualHns);

    if (FAILED(hr))
    {
        std::fwprintf(stderr, L"Failed. HRESULT = 0x%08lX\n", static_cast<unsigned long>(hr));
        return 1;
    }

    std::wprintf(L"Saved: %ls\n", argv[3]);
    std::wprintf(L"Requested: %.3f sec\n", HnsToSeconds(targetHns));
    std::wprintf(L"Actual: %.3f sec\n", HnsToSeconds(actualHns));
    return 0;
}

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

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

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

常見問題

整理諮詢這個主題時常見的問題。

要從 MP4 取出指定時間的靜態影像,該用什麼?
只抽一張的話,比起 Media Session,IMFSourceReader 當入口更直接了當。流程是用 MFCreateSourceReaderFromURL 開啟檔案,用 SetStreamSelection 只選影像串流,要求 MFVideoFormat_RGB32,用 SetCurrentPosition 做 seek,再用 ReadSample 取得影格,最後用 WIC 存成 PNG。不需要外部函式庫,只用 Windows 標準 API 就能完成。
SetCurrentPosition 能不能正好移動到指定時間?
不能。IMFSourceReader::SetCurrentPosition 不保證 exact seeking,影片通常會落在指定位置稍前,尤其會偏向 key frame 那一側。必須在 seek 之後繼續 ReadSample,一邊看 timestamp,一邊比較 target 之前的最後一個樣本與 target 之後的第一個樣本,採用比較接近的那一個。seek 之後只讀一次就儲存的實作,在 GOP 長的影片上很容易偏掉。
為什麼存出來的 PNG 會變透明?
因為 MFVideoFormat_RGB32 的第 4 個位元組不一定是 alpha。Windows 的 32bit RGB 中,byte 0、1、2 是 B、G、R,byte 3 可能是 alpha,也可能是 ignore(並不是 ARGB32)。直接寫進 PNG 有時會變成透明影像,所以在儲存前把第 4 個位元組填成 0xFF 讓它不透明比較安全。
影像亂掉或上下顛倒的原因是什麼?
原因在於 stride 與上下方向的處理。影像緩衝區不一定是以 width×bytesPerPixel 一路緊密排列,每列結尾可能有 padding,RGB 系也可能是 bottom-up(stride 為負)。用 IMF2DBuffer::Lock2D 取得 scan line 0 的起始指標與實際的 stride,先重新排成 top-down 的連續 BGRA 緩衝區再交給 PNG,儲存端會變單純,也能避免影像亂掉。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽