用 Media Foundation 從 MP4 影片指定時間點抽出靜止畫面的方法 - 可直接貼進 .cpp 的單檔完整版

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

「想從 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,還要看 flagspSample
  • 把輸出 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 是危險的。要把 HRESULTflagspSample 三件事一起看 才安全。

4.3. 隨便處理 stride 和上下方向,畫面就會壞

影像 buffer 並不一定是 width * bytesPerPixel 一直線排著的。行尾可能有 padding,RGB 系也可能是 bottom-up。這點在 Microsoft Learn 的 Image StrideUncompressed 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
  • BufferLock helper 取得 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 HRESULTflagspSample 都看 在尾端或 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.hstdafx.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 抽出指定時間的靜止畫面,光看 SetCurrentPositionReadSample 其實還不夠。實務上真正要留意的是:

  • seek 不是 exact
  • frame 要看 timestamp 做前後比較
  • ReadSample 成功也可能沒 sample
  • 存檔前要吸收 stride 與影像方向
  • 不要把 RGB32 的第 4 byte 當成 alpha

把這些抓住,出狀況的機率會大大降低。

這份範例以「好好抽出一張」為目標、做得相當精簡,用起來應該很順手。縮圖產生、監視影像代表 frame 保存、檢驗紀錄留證類用途,可以很自然地直接搬進去。

9. 參考資料

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

下面這一整塊,就是設計給你直接貼進 Visual Studio C++ 主控台應用程式專案用的程式碼。命令列引數依序是 input.mp4secondsoutput.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,
            &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;
}

相關文章

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

相關主題

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

與本主題相關的服務

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

回到部落格一覽