Estrazione di un'immagine fissa da un MP4 in un momento specifico con Media Foundation

· Aggiornato il: · · Media Foundation, C++, Sviluppo Windows, WIC

“Prendi il singolo fotogramma alla soglia dei 12,3 secondi di un MP4” è un requisito perfettamente comune: generazione di miniature, registri di ispezione, fotogrammi rappresentativi di filmati di sorveglianza, immagini di prova per registri di apparecchiature e così via.

In Media Foundation, tuttavia, questo è leggermente meno semplice di quanto sembri. Sembra che chiamare ReadSample una volta dopo SetCurrentPosition dovrebbe farlo, ma in pratica vengono coinvolti fotogrammi chiave, timestamp, passo, orientamento dell’immagine e il quarto byte di RGB32. Procedi con noncuranza e si verificano degli incidenti leggermente fastidiosi: l’ora è leggermente spostata, l’immagine è capovolta o il PNG risulta stranamente trasparente.

Per il quadro generale dello stesso Media Foundation, il nostro post precedente Cos’è Media Foundation? - Perché il volto di COM e dei media Windows API vengono alla luce è un utile compagno. Questa volta scendiamo di un livello e ci concentriamo esclusivamente sull’estrarre un singolo fotogramma da un MP4.

In questo articolo, utilizziamo IMFSourceReader per estrarre l’immagine fissa più vicina a un tempo specificato da un MP4 e salvarla come PNG, con tutte le insidie ​​​​che potresti incontrare nella pratica lungo il percorso. E alla fine c’è un unico elenco di codici autonomo che puoi incollare direttamente nel .cpp di un progetto di app console Visual Studio C++. Non ci sono frammenti di codice sparsi nell’articolo: prendi solo il blocco alla fine e funziona.

Il codice in questo articolo è pubblicato anche in GitHub come esempio completo (un’app console C++ autonoma e composta da un file).

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

1. Prima la conclusione

Riassumendo le conclusioni in anticipo:

  • Per estrarre un fotogramma da un MP4, Source Reader è un punto di ingresso più naturale rispetto a Media Session per questo lavoro
  • IMFSourceReader::SetCurrentPosition non garantisce una ricerca esatta. Normalmente arriva leggermente prima del target, inclinato verso un fotogramma chiave, quindi è necessario avanzare con ReadSample e confrontare i fotogrammi su entrambi i lati del tempo target
  • ReadSample può avere successo e darti comunque pSample == nullptr. Controlla flags e pSample, non solo HRESULT
  • Indirizzare l’uscita media type a MFVideoFormat_RGB32 semplifica il risparmio
  • Tuttavia, il quarto byte di RGB32 non è necessariamente alfa, quindi scriverlo direttamente su PNG può produrre un’immagine trasparente. È più sicuro impostarlo su 0xFF prima di salvare in modo che l’immagine sia opaca
  • Gestisci con noncuranza l’orientamento per riga stride e dall’alto verso il basso / dal basso verso l’alto e l’immagine si rompe, quindi l’approccio più semplice è normalizzare il campione estratto in un buffer top-down e contiguo BGRA prima di consegnarlo a PNG

In breve, seek -> read once -> save è un po’ sciatto; arrivare fino a seek -> compare around the target while watching timestamps -> copy with stride in mind -> save as PNG e le cose diventano abbastanza stabili.

2. Presupposti per questo articolo

2.1. A chi è rivolto e conoscenze richieste

  • Questo articolo si rivolge a chi ha già chiamato API COM di Windows da C++ (o da C#). Basta conoscere il flusso: inizializzare con CoInitializeEx e rilasciare i puntatori alle interfacce con Release (o l’equivalente C#).
  • Il valore restituito HRESULT si giudica con le macro SUCCEEDED / FAILED. In caso di fallimento, leggere il valore esadecimale così com’è. I valori che iniziano con 0x8007 provengono da errori Win32; i 16 bit inferiori sono il codice di errore Win32 (ad esempio 0x80070057 è lo stesso valore di E_INVALIDARG). Gli errori specifici di Media Foundation che iniziano con MF_E_ sono definiti in mferror.h.
  • Media Foundation in sé è scritto in modo che anche chi la tocca per la prima volta possa seguire. Il quadro generale dal punto di vista COM è trattato in Un’introduzione a Media Foundation: comprendere l’API attraverso una lente COM.

2.2. Ambiente di sviluppo

Elemento Presupposto per questo articolo
OS Windows 10 / Windows 11
IDE Visual Studio 2022 (carico di lavoro “Sviluppo di applicazioni desktop in C++”)
SDK Windows SDK incluso in Visual Studio (contiene intestazioni e librerie per Media Foundation e WIC)
Progetto Modello “App console” in C++
Piattaforma x64
Librerie aggiuntive Nessuna. mfplat.lib e simili sono collegate dai #pragma comment(lib, ...) nel codice

I dettagli su come incollare il codice e su eventuali intestazioni precompilate sono riassunti in 7. Note sulla compilazione e sull'esecuzione.

2.3. Input e output di questo esempio

Le nostre ipotesi questa volta:

  • L’input è un file MP4 locale
  • Vogliamo una singola immagine fissa
  • Restituiamo “il fotogramma più vicino all’ora specificata”, non “esattamente all’ora specificata”
  • L’implementazione utilizza IMFSourceReader in modalità sincrona
  • Il formato di output è PNG tramite WIC
  • Nessuna libreria esterna: solo Windows standard API
  • Un MP4 ordinario la cui risoluzione non cambia a metà flusso

Se hai bisogno di riproduzione, sincronizzazione audio, barra di ricerca o integrazione dell’UI, si applicano altri design, ma per “Voglio un fotogramma” questo approccio è molto facile da seguire.

3. Le tabelle da guardare per prime

3.1. Il flusso di elaborazione

Passo API utilizzata Ruolo
Apri MP4 MFCreateSourceReaderFromURL Crea una fonte multimediale dal file
Seleziona solo video SetStreamSelection Evitare di leggere l’audio
Converti in RGB32 SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING Ottieni frame non compressi facili da salvare
Passare all’ora target SetCurrentPosition Ricerca in unità di 100 ns
Leggi i frame ReadSample Recupera i campioni decodificati uno alla volta
Confronta prima / dopo timestamp di esempio Decidi il singolo fotogramma più vicino al target
Salva come PNG WIC Scrivi il file immagine

3.2. La regola di selezione per questo articolo

“L’immagine fissa a un momento specificato” sembra semplice, ma il video è costituito da fotogrammi distinti, non da un continuum, quindi è più semplice decidere in anticipo la regola di selezione.

Questa volta la regola è:

  • Avanzare con ReadSample dopo la ricerca
  • Mantieni l’ultimo campione con timestamp < target
  • When the first sample with timestamp >= target arrivato, confronta i delta dei campioni precedente e attuale
  • Adottare quello che è più vicino all’obiettivo

Ciò rende più semplice ottenere il fotogramma più vicino al target, anziché “il primo fotogramma in corrispondenza o dopo il target”.

3.3. La forma della lavorazione

Dall’inizio alla fine, il flusso è all’incirca input.mp4 -> create the Source Reader -> request RGB32 -> seek -> repeat ReadSample -> compare around the target -> repack into top-down BGRA -> save as PNG via WIC.

Sembra semplice, ma la precisione, i campioni nulli, il passo e il quarto byte nascondono ciascuno una piccola trappola. Evita di attraversarli e questo è davvero facile da usare.

4. Le insidie ​​da conoscere in anticipo

4.1. SetCurrentPosition non è una ricerca esatta

Come dice Microsoft Learn per IMFSourceReader::SetCurrentPosition, non garantisce la ricerca esatta. Per i video, normalmente arriva leggermente prima della posizione richiesta, inclinato verso un fotogramma chiave. L’aspettativa è che poi si avanzi alla posizione target con ReadSample.

Il che rende questo tipo di implementazione piuttosto traballante:

  • SetCurrentPosition(target)
  • Uno ReadSample
  • Salva quel fotogramma

Nei video con GOPs lungo, questo si allontana perfettamente dal bersaglio. Questa è la tranquilla zona di fango.

4.2. ReadSample può avere successo con pSample == nullptr

ReadSample può restituire S_OK mentre ppSample è NULL. Alla fine dello stream viene restituito il flag MF_SOURCE_READERF_ENDOFSTREAM; per le lacune nel flusso, MF_SOURCE_READERF_STREAMTICK e così via.

Controllare solo HRESULT e dereferenziare immediatamente pSample è pericoloso. L’abitudine sicura è guardarli tutti e tre: HRESULT, flags e pSample.

4.3. Maneggia stride e l’orientamento con noncuranza e l’immagine si rompe

Un buffer di immagine non è necessariamente compresso in una linea retta di width * bytesPerPixel. Le righe possono contenere il riempimento di fine riga e i formati RGB possono essere dal basso verso l’alto. Image Stride e Uncompressed Video Buffers di Microsoft Learn lo affermano abbastanza chiaramente.

I due punti particolarmente importanti:

  • IMF2DBuffer::Lock2D restituisce il puntatore all’inizio della linea di scansione 0 e il falcato effettivo
  • Per le immagini dal basso verso l’alto, il passo può essere negativo

Questo articolo adotta l’approccio degli helper Microsoft Learn e ricompone tutto in un buffer BGRA contiguo dall’alto verso il basso prima di passarlo a PNG. Risolvi prima questo e il lato del salvataggio diventa molto semplice.

4.4. Non dare per scontato che il quarto byte di MFVideoFormat_RGB32 sia alfa

Nonostante l’atmosfera del nome, MFVideoFormat_RGB32 non è “pulito RGBA”, puoi passare direttamente a PNG. Windows 32-bit RGB ha i byte 0, 1, 2 come B, G, R e il byte 3 può essere alfa o può essere ignorato. Il punto chiave è che non è ARGB32.

Supponiamo che sia GUID_WICPixelFormat32bppBGRA e salvatelo così com’è, e potreste trovare degli zeri nel quarto byte e un’immagine stranamente trasparente. La nostra politica qui è di riempire l’alfa con 0xFF prima di salvare in modo che l’immagine sia completamente opaca.

5. Il flusso di implementazione

5.1. Crea Source Reader in modalità sincrona

Poiché abbiamo bisogno di un solo frame, utilizziamo ReadSample sincrono anziché callback asincroni. In modalità sincrona, ReadSample si blocca fino al campione successivo, ma per l’estrazione di immagini fisse one-shot l’implementazione rimane molto semplice.

Quando creiamo il lettore, facciamo queste quattro cose:

  • MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  • Disattiva prima tutti gli stream
  • Accendi solo MF_SOURCE_READER_FIRST_VIDEO_STREAM
  • Imposta il tipo di output su MFMediaType_Video / MFVideoFormat_RGB32

Successivamente, il codice downstream può essere scritto partendo dal presupposto “riceviamo frame RGB32”.

5.2. Dopo la ricerca, avvicinati mentre guardi i timestamp

Dopo SetCurrentPosition, non salviamo immediatamente. Leggiamo i campioni con ReadSample, confrontando l’ultimo fotogramma prima del target con il primo fotogramma che attraversa il target.

Questo passaggio aggiuntivo assorbe la maggior parte della grossolanità della ricerca.

5.3. Normalizza il campione in top-down BGRA

Non scriviamo direttamente il campione estratto in PNG; per prima cosa lo reimpacchettamo in un buffer BGRA top-down.

  • Combina in un unico buffer con ConvertToContiguousBuffer
  • Ottieni la linea di scansione 0 e il passo effettivo tramite l’helper BufferLock
  • Copia riga per riga nel buffer dall’alto verso il basso
  • Imposta l’alfa su 0xFF

Ora il lato del salvataggio può trattarlo come “solo un’immagine BGRA a 32 bpp”.

5.4. Lascia PNG salvando su WIC

Il salvataggio utilizza IWICBitmapEncoder / IWICBitmapFrameEncode di WIC. Media Foundation recupera il frame; WIC lo trasforma in un’immagine. Il tutto rientra nello standard Windows API.

6. Una lista di controllo pratica

Articolo Cosa controllare Cosa tende ad accadere se mancato
Cerca la precisione Non decidere sulla lettura singola subito dopo SetCurrentPosition Salvi un frame molto prima dell’orario richiesto
Campioni nulli Controlla HRESULT, flags e pSample: tutti Dereferenziazione nulla alla fine dello stream o su uno stream tick
Passo Assorbi il passo reale e l’orientamento verticale L’immagine si rompe o esce capovolta
Il 4° byte di RGB32 Imposta alfa su 0xFF Un PNG trasparente
Intervallo di tempo Mantieni 0 <= target < duration Comportamento anomalo verso la fine dello stream
Estrazione ripetuta Ripeti le ricerche invece di ricreare il lettore Inutilmente lento
Conteggio copie Per l’elaborazione in blocco, tieni presente il costo di ConvertToContiguousBuffer CPU e larghezza di banda della memoria sprecate
Cambiamenti di formato Gestisci i video la cui risoluzione cambia durante lo streaming con un design separato I presupposti di larghezza / altezza non funzionano

7. Note sulla compilazione e sull’esecuzione

7.1. Note sulla compilazione

Il codice alla fine di questo articolo è strutturato per essere facile da aggiungere come singolo .cpp a un’app console Visual Studio C++.

Alcune cose che vale la pena sapere:

  • Sono incluse le direttive #pragma comment(lib, ...), quindi impostazioni aggiuntive del linker generalmente non sono necessarie
  • Viene utilizzato wmain, quindi gli argomenti della riga di comando vengono gestiti interamente in Unicode
  • Per i template predefiniti di app console con pch.h o stdafx.h, l’inizio del codice usa __has_include per rilevarli, in modo che anche incollandolo continui a funzionare
  • Se il progetto forza una propria intestazione precompilata, imposta “Not Using Precompiled Headers” solo per questo .cpp e verrà compilato
  • x64 è la configurazione consigliata

7.2. Esecuzione e output previsto

Uso: ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>. Ad esempio: ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png.

Se l’esecuzione ha successo, alla fine di wmain vengono scritte sullo standard output le seguenti 3 righe:

Saved: C:\work\frame.png
Requested: 12.345 sec
Actual: <timestamp del frame scelto> sec

Requested è il numero di secondi passato come argomento, mentre Actual è il timestamp del frame effettivamente adottato. Poiché in 3.2 prendiamo il frame più vicino tra quelli prima e dopo il target, la differenza tra i due valori rimane all’incirca entro la metà dell’intervallo tra due frame (per un video a 29,97 fps, circa 17 ms al massimo). Se la differenza è di qualche centinaio di ms o più, significa probabilmente che dopo la ricerca si è letto una sola volta e il confronto intorno al target descritto in 4.1 non ha funzionato.

Gli obiettivi della verifica sono questi tre punti:

  • Il codice di uscita è 0 e la riga Saved: riporta il percorso di output specificato
  • Aprendo il PNG prodotto, la scena all’istante richiesto è orientata correttamente (non capovolta)
  • Larghezza e altezza del PNG corrispondono alla risoluzione del video originale e lo sfondo non è trasparente (funziona il riempimento alpha di 4.4)

In caso di fallimento, sullo standard error viene stampato Failed. HRESULT = 0x......... Decodificalo come HRESULT standard di Windows: ad esempio 0x80070057 corrisponde a E_INVALIDARG e indica che il tempo specificato è maggiore o uguale alla durata del video.

Quando si estrae un’immagine fissa in un momento specifico da un MP4 con Media Foundation, guardare solo SetCurrentPosition e ReadSample non è sufficiente. In realtà:

  • La ricerca non è esatta
  • I frame dovrebbero essere confrontati attorno al target in base al timestamp
  • Un ReadSample riuscito potrebbe comunque non trasportare alcun campione
  • Assorbire stride e l’orientamento dell’immagine prima di salvare
  • Non dare per scontato che il quarto byte di RGB32 sia alfa

Copriteli e gli incidenti diventeranno davvero rari.

Essendo una configurazione minima incentrata sull’estrazione corretta di un fotogramma, questo esempio dovrebbe essere molto utilizzabile. Passa direttamente alla generazione delle miniature, salvando fotogrammi rappresentativi dai filmati di sorveglianza ed emettendo immagini di prova per i registri di ispezione.

9. Riferimenti

10. Il codice completo, pronto per essere incollato in un .cpp

Il singolo blocco seguente è pensato per essere portato direttamente in un progetto di app console Visual Studio C++. Gli argomenti della riga di comando sono, in ordine, input.mp4, seconds e output.png. È un layout autonomo a file singolo, quindi si incolla facilmente in un progetto.

#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);

        // The 4th byte of MFVideoFormat_RGB32 is not necessarily alpha,
        // so force it to opaque before saving as 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;
}

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Perché IMFSourceReader::SetCurrentPosition non cerca l'ora esatta che ho richiesto?
SetCurrentPosition non garantisce una ricerca esatta. Per i video, normalmente arriva leggermente prima della posizione richiesta, sbilanciata verso un fotogramma chiave, con l'aspettativa che poi si avanzi verso la destinazione con ReadSample. Nei video con GOPs lungo, il salvataggio del primo fotogramma dopo che la ricerca si allontana regolarmente dal bersaglio. L'approccio stabile consiste nel continuare a leggere i campioni, ricordare l'ultimo fotogramma prima del tempo target e quando arriva il primo fotogramma in corrispondenza o oltre il target, confrontare i due timestamp e adottare qualunque fotogramma sia più vicino.
Perché ReadSample restituisce S_OK ma mi fornisce un campione nullo?
ReadSample può avere esito positivo mentre ppSample è NULL. Alla fine dello stream restituisce il flag MF_SOURCE_READERF_ENDOFSTREAM e per le lacune dello stream restituisce MF_SOURCE_READERF_STREAMTICK, entrambi senza campione. Controllare solo HRESULT e dereferenziare immediatamente il puntatore di esempio è pericoloso: l'abitudine sicura è guardarli tutti e tre: HRESULT, i flag e il puntatore di esempio.
Perché il mio PNG estratto è trasparente o capovolto?
Ciò è causato da due trappole separate. La trasparenza proviene dal quarto byte di MFVideoFormat_RGB32: non è necessariamente alfa, quindi se salvi il buffer come 32bpp BGRA senza correggerlo, gli zeri in quel byte producono un'immagine stranamente trasparente: imposta alfa su 0xFF prima di salvare in modo che l'immagine sia opaca. Le immagini capovolte o spezzate derivano dal passo e dall'orientamento: le righe possono contenere riempimento e i formati RGB possono essere dal basso verso l'alto con un passo negativo, quindi reimballare tutto in un buffer BGRA top-down e contiguo prima di passarlo al codificatore PNG.
Ho bisogno di librerie esterne per estrarre un frame da un MP4 su Windows?
No. L'approccio in questo articolo utilizza solo Windows standard API: IMFSourceReader in modalità sincrona per aprire MP4, selezionare il flusso video, convertire l'output in RGB32 tramite MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, cercare e leggere i fotogrammi, quindi WIC (IWICBitmapEncoder) per scrivere PNG. Per l'estrazione di immagini fisse one-shot, il Source Reader sincrono è un punto di ingresso più naturale rispetto al Media Session e l'articolo termina con un singolo elenco C++ autonomo che puoi incollare in un'app console Visual Studio.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog