用 Media Foundation 從 MP4 影片指定時間點抽出靜止畫面的方法 - 可直接貼進 .cpp 的單檔完整版
「想從 MP4 抽出第 12.3 秒那一張」的需求其實相當常見。像是做縮圖、檢驗紀錄、監視影像的代表畫面、裝置紀錄的留證等等。
只不過 Media Foundation 在這件事上並沒有那麼直覺。看起來好像 SetCurrentPosition 之後呼叫一次 ReadSample 就完事了,但實際上會牽扯 key frame、timestamp、stride、影像上下方向、RGB32 的第 4 byte 等細節。做得草率的話,就會發生時間點偏掉一點、畫面上下顛倒、PNG 莫名變透明等,這種低調但讓人煩躁的小狀況。
Media Foundation 的整體觀念,之前寫過的 Media Foundation 是什麼 - 為什麼看得到 COM 與 Windows 媒體 API 的影子 可以一起參考。本文則再往下一層,只聚焦在 從 MP4 抓一張畫面。
本文會用 IMFSourceReader,把「從 MP4 抓出最靠近指定時間點的靜止畫面一張,並存成 PNG」這件事,連同實務上常踩的坑一起整理。最後還附了一份 可直接貼進 Visual Studio C++ 主控台應用程式專案 .cpp 的單檔完整程式碼,過程中不會有切開貼散的程式碼,只要把最後那一整塊拿走就能跑。
1. 先說結論
先把結論濃縮一下:
- 從 MP4 取一張圖的話,這次用
Source Reader當入口比Media Session自然 IMFSourceReader::SetCurrentPosition並不保證 exact seek。通常會落在 target 之前,尤其偏向 key frame,所以後面得用ReadSample一邊往前走一邊比對目標時間的前後ReadSample就算成功,pSample == nullptr也是有可能的。不能只看HRESULT,還要看flags和pSample- 把輸出 media type 設成
MFVideoFormat_RGB32會比較好存 - 不過
RGB32的第 4 byte 不一定是 alpha,直接寫成 PNG 可能變透明。存檔前把它塞成0xFF讓它不透明比較安全 - 每列的
stride和 top-down / bottom-up 處理不好的話,畫面就會壞掉。抽出來的 sample 先整成 top-down 連續 BGRA 再丟給 PNG 會比較輕鬆
簡單說,seek -> 讀一次 -> 存 就太粗糙了。做到 seek -> 看 timestamp 比對前後 -> 注意 stride 做複製 -> 寫 PNG 這樣的程度,整體會穩定很多。
2. 本文前提
以下前提進行:
- 輸入是本機的 MP4 檔
- 想要一張靜止畫面
- 不是「正好在指定時間」,而是「最靠近指定時間的那一張」
- 實作用同步模式的
IMFSourceReader - 存檔格式是用 WIC 寫出來的 PNG
- 不借助外部函式庫,只用 Windows 標準 API
- 以中途解析度不會改變的一般 MP4 為前提
如果要做到播放、音訊同步、seek bar、UI 連動,那是另一個設計題目;但只要「抓一幀」這種用途,這樣做就很清楚。
3. 先看整理表
3.1. 處理流程
| 要做的事 | 使用的 API | 角色 |
|---|---|---|
| 開啟 MP4 | MFCreateSourceReaderFromURL |
從檔案建立 media source |
| 只選影像 | SetStreamSelection |
避免讀音訊 |
| 轉成 RGB32 | SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING |
拿到方便存檔的 uncompressed frame |
| 移到指定時間 | SetCurrentPosition |
以 100ns 為單位 seek |
| 讀一幀 | ReadSample |
一幀一幀取解碼後的 sample |
| 比較前後 | sample timestamp | 決定最靠近指定時間的那一張 |
| 存成 PNG | WIC | 寫進圖片檔 |
3.2. 這次的判定規則
雖然說「指定時間的靜止畫面」,但影片本來就不是連續量,是離散的幀。所以實作時,先決定好用什麼規則挑一張會比較輕鬆。
這次採用下列規則:
- seek 後繼續
ReadSample - 保留
timestamp < target的最後一個 sample - 只要遇到
timestamp >= target的第一個 sample,就把它跟前一個 sample 與 target 的差做比較 - 選靠 target 比較近的那個
這樣比起「target 之後第一張」,更容易拿到 最靠近 target 的那一張。
3.3. 處理示意
整體流程大致是 input.mp4 -> 建立 Source Reader -> 要求 RGB32 -> seek -> 反覆 ReadSample -> 比較 target 的前後 -> 填成 top-down BGRA -> 用 WIC 存 PNG。
看起來很單純,但 seek 精度、sample 是 null、stride、第 4 byte 的處理,各自都有小陷阱。只要這些不踩,用起來其實滿自然的。
4. 先抓住的陷阱
4.1. SetCurrentPosition 不是 exact seek
如同 Microsoft Learn 的 IMFSourceReader::SetCurrentPosition 所述,這個函式並不保證 exact seeking。影片的情況下通常會落在指定位置稍前,特別是往 key frame 方向靠。然後還預期你接著用 ReadSample 往前走到目標位置。
所以下面這種寫法其實很危險:
SetCurrentPosition(target)- 呼叫一次
ReadSample - 把那張存下來
在 GOP 長的影片裡,這樣偏掉是家常便飯。這就是一個低調的泥沼。
4.2. ReadSample 成功也可能 pSample == nullptr
ReadSample 即使回 S_OK,ppSample 也可能是 NULL。如果是到尾端就會回 MF_SOURCE_READERF_ENDOFSTREAM,遇到 stream 空白就會回 MF_SOURCE_READERF_STREAMTICK 等 flag。
所以只看 HRESULT 就直接碰 pSample 是危險的。要把 HRESULT、flags、pSample 三件事一起看 才安全。
4.3. 隨便處理 stride 和上下方向,畫面就會壞
影像 buffer 並不一定是 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 buffer 再丟給 PNG。先把這裡整理好,存檔那段就會變得相當單純。
4.4. 不要把 MFVideoFormat_RGB32 的第 4 byte 當成 alpha
MFVideoFormat_RGB32 聽起來一副「能直接丟給 PNG 的漂亮 RGBA」,但其實不是。Windows 的 32bit RGB 是 byte 0、1、2 為 B、G、R,而 byte 3 可能是 alpha,也可能只是 ignore。這不是 ARGB32,這一點很重要。
如果在這裡把它當成 GUID_WICPixelFormat32bppBGRA 直接存,第 4 byte 可能還是 0,結果畫面就變得很透明。本文採用 存檔前把 alpha 填 0xFF 讓整張完全不透明 的做法。
5. 實作流程
5.1. 用同步模式建立 Source Reader
這次只需要一張畫面,所以不用非同步 callback,改用同步 ReadSample。同步模式下 ReadSample 會 block 到下一個 sample,但對單次擷取靜止畫面來說,實作非常直覺。
建立 Reader 時要做的事:
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE- 先關掉所有 stream
- 只開
MF_SOURCE_READER_FIRST_VIDEO_STREAM - 把輸出 type 設成
MFMediaType_Video/MFVideoFormat_RGB32
這樣後面的處理就可以以「接 RGB32 畫面」為前提來寫。
5.2. seek 之後看 timestamp 往下跑
SetCurrentPosition 之後不要馬上存檔。邊用 ReadSample 讀,邊把 target 之前的最後一張和跨過 target 的第一張做比較。
這個多走一步,就能把 seek 的粗糙吸收掉大部分。
5.3. 把 sample 整成 top-down BGRA
抽出來的 sample 不要直接丟 PNG,先整成 top-down 的 BGRA buffer:
- 用
ConvertToContiguousBuffer整成單一 buffer - 用
BufferLockhelper 取得 scan line 0 和實際 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 | 無謂地變慢 |
| 複製次數 | 大量處理時要留意 ConvertToContiguousBuffer 的成本 |
多吃 CPU 和記憶體頻寬 |
| 格式變更 | 中途會改解析度的特殊影片要改設計 | 寬高前提被打破 |
7. 建置與執行備註
本文末尾的程式碼,設計成 可以直接當成一個 .cpp 加進 Visual Studio C++ 主控台應用程式。
記住下面幾點會輕鬆很多:
- 已放入
#pragma comment(lib, ...),基本不用再設定連結器 - 用的是
wmain,所以命令列引數可以直接以 Unicode 處理 - 預設 Console App 範本若有
pch.h或stdafx.h,程式最上方也用__has_include自動引入 - 若專案硬性要求自訂的預先編譯標頭,只要把這個
.cpp改成「不使用預先編譯標頭」即可通過 - 建議用 x64 組態執行
用法是 ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>。例如 ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png 這樣執行。
8. 小結
用 Media Foundation 從 MP4 抽出指定時間的靜止畫面,光看 SetCurrentPosition 和 ReadSample 其實還不夠。實務上真正要留意的是:
- seek 不是 exact
- frame 要看 timestamp 做前後比較
ReadSample成功也可能沒 sample- 存檔前要吸收
stride與影像方向 - 不要把
RGB32的第 4 byte 當成 alpha
把這些抓住,出狀況的機率會大大降低。
這份範例以「好好抽出一張」為目標、做得相當精簡,用起來應該很順手。縮圖產生、監視影像代表 frame 保存、檢驗紀錄留證類用途,可以很自然地直接搬進去。
9. 參考資料
- 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 的完整程式碼
下面這一整塊,就是設計給你直接貼進 Visual Studio C++ 主控台應用程式專案用的程式碼。命令列引數依序是 input.mp4、seconds、output.png。已經做成單檔完整的形式,很好貼進專案。
#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 影片每一幀的方法 - 整理 Source Reader / 繪製 / 色彩轉換 / Sink Writer 與可直接貼進 .cpp 的單檔完整版
整理用 Media Foundation 把圖片與文字烙印到 MP4 每一幀的整體流程,把 Source Reader 解碼、GDI+ 繪製、BGRA 與 NV12 的色彩轉換、Sink Writer 重新編碼四段拆開來看,並附上可直接貼進 Visual Studio 主控...
在 Media Foundation 中把 YUV 畫面轉成 RGB 的方法 - 從原理整理 Source Reader 自動轉換與自行轉換
本文整理在 Media Foundation 中把解碼後的 YUV 影像轉成 RGB 的兩種做法。讀完之後,可以看清楚何時該交給 Source Reader 自動產生 RGB32、何時需要自行接住 NV12 或 YUY2,並掌握 BT.601 與 BT.709 矩陣、lim...
Media Foundation 是什麼 - COM 和 Windows 媒體 API 的臉浮現的原因
整理 Media Foundation 為何讓人覺得像 COM:其本體是媒體處理平台,但 source、transform、sink、callback 之間的邊界以 COM 介面表現,因此 HRESULT、GUID、apartment 自然浮現,並指出 Source Rea...
使用共享記憶體時的陷阱與最佳實踐 - 先整理同步、可見性、壽命、ABI、安全性
整理在同一機器內以共享記憶體交換大型資料時的陷阱與設計要點。把 control plane 和 data plane 分離、縮小並行模型、用固定寬度整數和標頭設計 ABI、以 offset 取代指標、明示 commit protocol、為當機復原放入 generation...
用 Native AOT 把 C# 做成原生 DLL 的方法 - 用 UnmanagedCallersOnly 從 C/C++ 呼叫
從現有 C/C++ 應用程式以 in-process 方式呼叫 C# 邏輯時,本文示範以 Native AOT 將類別庫發佈為原生 DLL,並用 UnmanagedCallersOnly 公開 cdecl 進入點。透過 handle、錯誤碼與扁平 C ABI 設計交界面,整...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。