Media FoundationでMP4の指定時刻から静止画を切り出す方法
· 更新日: · 小村 豪 · Media Foundation, C++, Windows開発, WIC
更新履歴(4件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- 対象読者と開発環境、入力と出力の前提を分けて整理しました。コード中の関数と本文の対応表、ビルドと実行のしかたと成功したときに出るもの、用語表を追加しました。実行結果の具体値は実測していないため書かず、差がフレーム間隔の半分以内に収まるという記事内の論理から導ける説明にしています。
- 本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589638)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「Media FoundationでMP4の指定時刻から静止画を切り出す方法」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589638 https://staging.comcomponent.com/blog/2026/03/15/000-media-foundation-extract-still-image-from-mp4-at-specific-time/
- DOI(最新版)
- 10.5281/zenodo.21589638
- DOI(この版)
- 10.5281/zenodo.21732662
MP4 から「12.3 秒地点の 1 枚」を取りたい、という要件はかなり普通にあります。サムネイル生成、検査ログ、監視映像の代表フレーム、装置ログの証跡などです。
ただ、Media Foundation ではここが少しだけ素直ではありません。SetCurrentPosition のあとに ReadSample を 1 回呼べば終わりに見えるのですが、実際には key frame、timestamp、stride、画像の上下向き、RGB32 の 4 バイト目などが絡みます。雑に進めると、時刻が少しズレる、画像が上下逆になる、PNG が妙に透明になる、という地味に嫌な事故が起きます。
Media Foundation の全体像そのものは、以前書いた Media Foundation入門 - COM視点でAPIを理解する も参考になります。今回はそこから一段降りて、MP4 から 1 枚抜く ところだけに絞ります。
この記事では、IMFSourceReader を使って、MP4 から 指定時刻に最も近い静止画を 1 枚取り出し、PNG として保存する ところまでを、実務で踏みやすい落とし穴込みで整理します。しかも最後には、Visual Studio の C++ コンソールアプリプロジェクトの .cpp にそのまま貼り付けやすい 1 ファイル完結コードを置いてあります。記事中の細切れコードはなしで、最後の 1 ブロックだけ持っていけば動く構成です。
なお、この記事に登場するコードは、サンプルコード一式(1 ファイル完結の C++ コンソールアプリ)として GitHub で公開しています。
media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)
1. まず結論
先に結論だけまとめると、こうです。
- MP4 から 1 枚抜くなら、今回は
Media SessionよりSource Readerのほうが入口として素直です 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 を雑に扱うと画像が崩れるので、取り出した sample は一度 top-down の連続 BGRA に整えてから PNG に渡します
要するに、seek -> 1 回読む -> 保存 では少し雑で、seek -> timestamp を見ながら前後比較 -> stride を意識してコピー -> PNG 保存 くらいまでやると、かなり安定します。
この記事の知識マップ
この記事は、Media FoundationのIMFSourceReaderを使ってMP4から指定時刻に最も近い1枚の静止画を取り出す方法を整理している。SetCurrentPositionによるseekはexactではなく通常キーフレーム側へ寄るため、GOPが長い動画ほど誤差が生じやすく、seek後にReadSampleを繰り返してtimestampの前後を比較する必要がある。ReadSampleは成功していてもサンプルがnullのことがあるため確認が要る。取り出したMFVideoFormat_RGB32のフレームは、strideと上下向きをIMF2DBuffer::Lock2Dで吸収してtop-downのBGRAへ整形し、4バイト目をalphaとして0xFFに固定してから、WICでPNGとして保存することで、意図しない透明PNGになる不具合を防いでいる。
flowchart LR
accTitle: MP4からの静止画抽出(Media Foundation)の知識マップ
accDescr: IMFSourceReaderによるSetCurrentPositionのseekがキーフレーム側へ寄ること、GOPの長さとReadSampleでの前後比較の関係、strideとtop-down/bottom-upの向きをLock2Dで吸収する処理、RGB32の4バイト目をalphaとして固定してWICでPNG保存するまでの関係を示す図。
video_frame_extraction["動画からの静止画抽出(指定時刻のフレーム取得)"]
imfsourcereader["IMFSourceReader(Source Reader)"]
media_foundation["Media Foundation"]
source_reader_seek["SetCurrentPositionによるseek"]
readsample["IMFSourceReader::ReadSample"]
group_of_pictures["GOP(Group of Pictures)"]
key_frame["キーフレーム(key frame)"]
video_stride["stride(画像の行送りバイト数)"]
imf2dbuffer_lock2d["IMF2DBuffer::Lock2D"]
top_down_bottom_up_orientation["top-down / bottom-up(画像の上下向き)"]
mfvideoformat_rgb32["MFVideoFormat_RGB32"]
unintended_transparent_png["意図しない透明PNG"]
alpha_channel_fixup["alphaの0xFF固定"]
windows_imaging_component["WIC(Windows Imaging Component)"]
null_sample_result["ReadSampleのnullサンプル"]
video_frame_extraction -->|"利用する"| imfsourcereader
imfsourcereader -->|"前提とする"| media_foundation
imfsourcereader -->|"利用する"| source_reader_seek
source_reader_seek -->|"前提とする"| readsample
group_of_pictures -.->|"原因になり得る"| source_reader_seek
source_reader_seek -->|"利用する"| key_frame
video_frame_extraction -->|"前提とする"| readsample
video_frame_extraction -->|"前提とする"| video_stride
video_stride -->|"利用する"| imf2dbuffer_lock2d
video_stride -->|"前提とする"| top_down_bottom_up_orientation
mfvideoformat_rgb32 -.->|"原因になり得る"| unintended_transparent_png
alpha_channel_fixup -->|"防止する"| unintended_transparent_png
video_frame_extraction -->|"利用する"| windows_imaging_component
video_frame_extraction -->|"利用する"| mfvideoformat_rgb32
readsample -->|"原因になり得る"| null_sample_result
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全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 ファイル
- 欲しいのは 1 枚の静止画
- 「指定時刻ぴったり」ではなく、「指定時刻に最も近いフレーム」を返す
- 実装は同期モードの
IMFSourceReader - 保存形式は WIC を使った PNG
- 外部ライブラリは使わず、Windows 標準 API だけで完結させる
- 途中で解像度が変わらない、一般的な MP4 を前提にする
再生、音声同期、シークバー、UI 連携までやるなら別の設計もありますが、1 フレーム取りたい という用途ならこれがかなり分かりやすいです。
3. まず見る整理表
3.1. 処理の流れ
| やること | 使う API | 役割 |
|---|---|---|
| MP4 を開く | MFCreateSourceReaderFromURL |
ファイルからメディアソースを作る |
| 動画だけ選ぶ | SetStreamSelection |
音声を読まないようにする |
| RGB32 に変換する | SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING |
保存しやすい uncompressed frame を得る |
| 指定時刻へ移動する | SetCurrentPosition |
100ns 単位で seek する |
| フレームを読む | ReadSample |
デコード済みサンプルを 1 枚ずつ取得する |
| 直前 / 直後を比較する | sample timestamp | 指定時刻に最も近い 1 枚を決める |
| PNG へ保存する | WIC | 画像ファイルへ書き出す |
3.2. 今回の判定ルール
「指定時刻の静止画」と言っても、動画は連続量ではなく離散フレームなので、実装ではどのルールで 1 枚を選ぶかを先に決めたほうが楽です。
今回は次のルールにします。
- seek 後に
ReadSampleを進める timestamp < targetの最後の sample を保持するtimestamp >= targetの最初の sample が来たら、直前 sample と現在 sample の差を比較する- より target に近いほうを採用する
これで「target 以降の最初の 1 枚」ではなく、target に最も近い 1 枚 を取りやすくなります。
3.3. 処理イメージ
処理の頭から末尾までは、だいたい input.mp4 -> Source Reader を作る -> RGB32 を要求する -> seek する -> ReadSample を繰り返す -> target の前後を比較する -> top-down BGRA に詰め直す -> WIC で PNG 保存 です。
見た目は単純ですが、seek の精度、sample の null、stride、4 バイト目の扱いにそれぞれ小さな罠があります。この 4 つを踏み抜かなければ、実装自体は素直に収まります。
4. 先に押さえる落とし穴
この章から出てくる用語を先に短くまとめておきます。
| 用語 | 意味 |
|---|---|
| key frame(キーフレーム) | 前後のフレームを参照せず、それ単独でデコードできるフレームです。H.264 では IDR ピクチャーなどが該当します |
| GOP(Group of Pictures) | ある key frame から次の key frame の直前までのフレームのまとまりです。GOP が長いほど key frame の間隔が空くので、seek 後の位置が指定時刻から遠くなりやすくなります |
| stride | 画像バッファで 1 行分が占めるバイト数です。幅 × 1 画素のバイト数 と一致するとは限らず、行末に padding が入ることがあります |
| top-down / bottom-up | 画像バッファの先頭が上端の行か下端の行かの違いです。bottom-up は stride が負の値で表現されます |
MF_SOURCE_READERF_STREAMTICK |
ReadSample が返す flag の 1 つで、ストリームにギャップ(データの途切れ)があることを示します。この flag が立った呼び出しではフレームが得られないので、読み直します |
4.1. SetCurrentPosition は exact seek ではない
Microsoft Learn の IMFSourceReader::SetCurrentPosition にもある通り、これは exact seeking を保証しません。動画では通常、指定位置の少し前、特に key frame 側へ寄ります。さらに、そのあと ReadSample を進めて目的位置まで前進する前提です。
このため、こういう実装はかなり危ういです。
SetCurrentPosition(target)ReadSampleを 1 回- その frame を保存
GOP が長い動画では、これでズレます。key frame の間隔が 2 秒空いている動画なら、指定時刻より最大で 2 秒近く手前のフレームを保存してしまうことになります。
4.2. ReadSample は成功でも pSample == nullptr があり得る
ReadSample は S_OK でも ppSample が NULL のことがあります。終端なら MF_SOURCE_READERF_ENDOFSTREAM、ストリームギャップなら MF_SOURCE_READERF_STREAMTICK などの flag が返ります。
HRESULT だけ見て pSample を即参照するのは危険です。HRESULT, flags, pSample を 3 点セットで見る くらいが安全です。
4.3. stride と上下向きを雑に扱うと画像が崩れる
画像バッファは width * bytesPerPixel で一直線に詰まっているとは限りません。行末に padding が入ることもありますし、RGB 系は bottom-up になることもあります。Microsoft Learn の Image Stride と Uncompressed Video Buffers でも、この点はかなりはっきり書かれています。
特に大事なのは次の 2 点です。
IMF2DBuffer::Lock2Dは scan line 0 の先頭ポインタ と 実際の stride を返す- bottom-up 画像では stride が負になることがある
今回は Microsoft Learn の helper の考え方を取り込み、最終的に top-down の連続 BGRA バッファ に詰め直してから PNG に渡します。ここを先に整えておくと、保存側がかなり単純になります。
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 で埋めて完全不透明にする 方針にします。
5. 実装の流れ
5.1. Source Reader を同期モードで作る
今回は 1 枚だけ取れればよいので、非同期 callback ではなく同期 ReadSample にします。同期モードでは ReadSample が次の sample まで block しますが、単発の静止画抽出なら実装がかなり素直です。
Reader の作成時にやるのはこの 4 つです。
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE- すべての stream をいったん off
MF_SOURCE_READER_FIRST_VIDEO_STREAMだけ on- 出力 type を
MFMediaType_Video/MFVideoFormat_RGB32に設定
これで、後段は「RGB32 のフレームを受け取る」前提で書きやすくなります。
5.2. seek のあとで timestamp を見ながら詰める
SetCurrentPosition のあと、いきなり保存しません。ReadSample で sample を読みながら、target より前の最後の 1 枚と、target をまたいだ最初の 1 枚を比較します。
この一手間で、seek の粗さをかなり吸収できます。
5.3. sample から top-down BGRA へ整形する
取り出した sample はそのまま PNG に書かず、いったん top-down の BGRA バッファに詰め直します。
ConvertToContiguousBufferで 1 本の buffer にするBufferLockhelper で scan line 0 と actual stride を得る- 行ごとに top-down buffer へコピーする
- alpha を
0xFFにする
これで保存側は「ただの 32bpp BGRA 画像」として扱えます。
5.4. PNG 保存は WIC に任せる
保存は WIC の IWICBitmapEncoder / IWICBitmapFrameEncode を使います。Media Foundation でフレームを取り、WIC で画像化する、という分担です。ここは Windows の標準 API だけで完結します。
6. 実務でのチェックリスト
| 項目 | 見ること | 見落とすと起きやすいこと |
|---|---|---|
| seek 精度 | SetCurrentPosition の直後 1 回で決めない |
指定時刻よりかなり前の frame を保存する |
| sample の NULL | HRESULT, flags, pSample を全部見る |
終端や stream tick で null dereference する |
| stride | actual stride と上下向きを吸収する | 画像が崩れる、上下逆になる |
| RGB32 の 4 byte 目 | alpha を 0xFF にする |
透明 PNG になる |
| 時刻範囲 | 0 <= target < duration を守る |
終端付近で意図しない挙動になる |
| 連続抽出 | Reader を作り直さず seek を繰り返す | 無駄に遅い |
| copy 回数 | 大量処理では ConvertToContiguousBuffer のコストを意識する |
CPU とメモリ帯域を余計に使う |
| フォーマット変化 | 途中で解像度が変わる特殊動画は別設計にする | 幅・高さ前提が壊れる |
このうち「連続抽出」と「copy 回数」の 2 行だけは、1 枚抽出そのものではなく、同じ動画から何十枚も抜くように広げたときに効いてくる項目です。今回のサンプルは 1 枚取って終了する構成なので、複数枚を抜くときは Source Reader を作り直さずに SetCurrentPosition と ReadSample の繰り返しへ変えてください。Source Reader 以外の選択肢(Media Session など)を含めた Media Foundation 全体の地図は Media Foundation入門 - COM視点でAPIを理解する にあります。
7. ビルドと実行のメモ
この記事の最後のコードは、Visual Studio の C++ コンソールアプリに 1 本の .cpp として追加しやすい形にしてあります。環境そのものの前提は 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 後に 1 回しか読んでいないなど、4.1 の前後比較が効いていないことを疑ってください。
動作確認のゴールはこの 3 点です。
- 終了コードが 0 で、
Saved:の行に指定した出力パスが出ている - 出力された PNG を開くと、その時刻のシーンが正しい向き(上下が逆でない)で表示される
- PNG の幅・高さが元動画の解像度と一致し、背景が透けていない(4.4 の alpha 埋めが効いている)
失敗した場合は標準エラー出力に Failed. HRESULT = 0x........ が出ます。値は 2.1 の要領で読みます。指定秒数が動画の長さ以上のときは 0x80070057(E_INVALIDARG)になります。
8. まとめ
Media Foundation で MP4 から指定時刻の静止画を取り出すときは、SetCurrentPosition と ReadSample だけ見ていると少し足りません。実際には、
- seek は exact ではない
- frame は timestamp を見て前後比較したほうがよい
ReadSample成功でも sample が無いことがあるstrideと画像の向きを吸収してから保存するRGB32の 4 バイト目は alpha と決め打ちしない
このあたりまで押さえておくと、かなり事故りにくくなります。
今回のサンプルは、1 枚をちゃんと抜く ことに絞った最小構成です。サムネイル生成、監視映像の代表 frame 保存、検査ログの証跡出力あたりには、そのまま持ち込める形にしてあります。
9. 参考資料
- この記事のサンプルコード一式: media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)
- Microsoft Learn: Using the Source Reader to Process Media Data
- Microsoft Learn:
IMFSourceReader::SetCurrentPosition - Microsoft Learn:
IMFSourceReader::ReadSample - Microsoft Learn:
IMFSourceReader::SetCurrentMediaType - Microsoft Learn:
IMF2DBuffer - Microsoft Learn:
IMF2DBuffer::Lock2D - Microsoft Learn: Uncompressed Video Buffers
- Microsoft Learn: Image Stride
- Microsoft Learn: MF_MT_FRAME_SIZE attribute
- Microsoft Learn: MF_MT_DEFAULT_STRIDE attribute
- Microsoft Learn: Native pixel formats overview (WIC)
- Microsoft Learn: Uncompressed RGB Video Subtypes
10. .cpp にそのまま貼れるフルコード
以下の 1 ブロックが、そのまま Visual Studio の C++ コンソールアプリプロジェクトへ持っていく前提のコードです。コマンドライン引数は input.mp4、seconds、output.png の順です。1 ファイル完結の構成にしてあるので、プロジェクトへ貼り付けやすい形です。
長い 1 ブロックなので、どの関数が本文のどこに対応しているかを先に置いておきます。読むときも、動かなかったときの調査でも、この対応表から入るのが早いです。
| コード中の関数・クラス | 対応する本文 | 役割と、対処している落とし穴 |
|---|---|---|
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 の前後を比較して 1 枚選ぶ。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,
¤tTimestamp,
&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;
}
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Media FoundationでMP4に画像と文字を焼き込む方法
Media Foundation で MP4 動画の各フレームへ画像と文字を焼き込み、新しい MP4 を作る考え方を、Source Reader、描画、色変換、Sink Writer の分担と 1 ファイル完結の C++ サンプルで整理します。
Media FoundationでYUVをRGBに変換する方法
Media FoundationでYUVフレームをRGBへ変換する方法を、Source Readerの自動変換とNV12/YUY2自前変換、stride、色空間から整理します。
Media Foundation入門 - COM視点でAPIを理解する
Media Foundation とは何かを、COM、HRESULT、IMFSourceReader、MFT など Windows メディア API の基本用語とあわせて、最初に押さえる順番で整理します。
共有メモリの落とし穴と実務ベストプラクティス
共有メモリを実務で使うときの落とし穴と、同期、可視性、寿命、ABI、権限まで含めた事故率を下げる設計を整理します。
C# Native AOT DLLをC/C++から呼び出す方法
C# のクラスライブラリを Native AOT でネイティブ DLL として発行し、UnmanagedCallersOnly のエントリポイントを C/C++ から呼び出す構成を、使いどころと実装パターンと注意点から整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
Media Foundation、Source Reader、WIC を使って動画から静止画を取り出す話なので、Windowsアプリ開発の実装テーマに近いです。
技術相談・設計レビュー
seek 精度、バッファ形式、stride、画像の向きなどを実装前に整理したい場合は、技術相談・設計レビューとして方針整理から入れます。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- MP4から指定時刻の静止画を取り出すには何を使えばよいですか?
- 1枚抜くなら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直前の最後のsampleとtarget以降の最初のsampleを比較し、より近いほうを採用する実装が必要です。seek後に1回だけ読んで保存する実装は、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に渡すと、保存側が単純になり崩れも防げます。