Extraire une image fixe d'un MP4 à un instant précis avec Media Foundation

· Mis à jour le: · · Media Foundation, C++, Développement Windows, WIC

« Extraire l’unique frame se trouvant à 12,3 secondes d’un MP4 » est une exigence tout à fait courante : génération de miniatures, journaux d’inspection, frame représentative d’une vidéo de surveillance, preuves pour des journaux d’équipement, etc.

Pourtant, avec Media Foundation, ce n’est pas tout à fait aussi simple. On pourrait croire qu’il suffit d’appeler ReadSample une seule fois après SetCurrentPosition, mais en réalité entrent en jeu la key frame, le timestamp, le stride, l’orientation de l’image et le quatrième octet de RGB32. Si l’on avance sans précaution, on obtient ces petits accidents discrets mais agaçants : l’instant est légèrement décalé, l’image se retrouve à l’envers, ou le PNG devient étrangement transparent.

Pour la vue d’ensemble de Media Foundation lui-même, notre article précédent Qu’est-ce que Media Foundation ? - Pourquoi le visage de COM et des API multimédias Windows devient visible est un bon complément. Cette fois, on descend d’un cran pour se concentrer uniquement sur l’extraction d’une seule frame d’un MP4.

Dans cet article, on utilise IMFSourceReader pour extraire l’image fixe la plus proche d’un instant donné d’un MP4 et l’enregistrer en PNG, en couvrant au passage les pièges réellement rencontrés en pratique. Et à la fin figure un unique listing de code autonome que vous pouvez coller directement dans le .cpp d’un projet d’application console C++ Visual Studio. Aucun fragment de code éparpillé dans l’article : il suffit de récupérer ce seul bloc final pour que cela fonctionne.

Le code de cet article est également publié sur GitHub sous forme d’exemple complet (une application console C++ autonome tenant en un seul fichier).

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

1. La conclusion d’abord

Pour résumer d’emblée les conclusions :

  • Pour extraire une seule frame d’un MP4, le Source Reader est ici un point d’entrée plus direct que la Media Session
  • IMFSourceReader::SetCurrentPosition ne garantit pas un seek exact. Il se positionne normalement juste avant la cible, avec un biais vers une key frame, il faut donc ensuite avancer avec ReadSample et comparer les instants situés avant et après l’heure visée
  • ReadSample peut réussir tout en renvoyant pSample == nullptr. Il faut vérifier flags et pSample, pas seulement le HRESULT
  • Orienter le type de média de sortie vers MFVideoFormat_RGB32 facilite l’enregistrement
  • Cependant, le 4e octet du RGB32 n’est pas forcément l’alpha : l’écrire tel quel en PNG peut produire une image transparente. Le plus sûr est d’y mettre 0xFF avant l’enregistrement pour la rendre opaque
  • Traiter à la légère le stride par ligne et l’orientation top-down / bottom-up déforme l’image ; le plus simple est de reconditionner l’échantillon extrait en un buffer BGRA contigu et top-down avant de le transmettre au PNG

En résumé, seek -> lire une fois -> enregistrer est un peu léger ; en allant jusqu’à seek -> comparer avant/après en surveillant le timestamp -> copier en tenant compte du stride -> enregistrer en PNG, le résultat devient nettement plus stable.

2. Les prérequis de cet article

Voici les hypothèses retenues ici :

  • L’entrée est un fichier MP4 local
  • Ce que l’on veut, c’est une seule image fixe
  • On renvoie « la frame la plus proche de l’instant demandé », pas « exactement l’instant demandé »
  • L’implémentation utilise IMFSourceReader en mode synchrone
  • Le format d’enregistrement est le PNG via WIC
  • Aucune bibliothèque externe : tout repose uniquement sur les API standard de Windows
  • On suppose un MP4 ordinaire dont la résolution ne change pas en cours de route

Si vous avez besoin de lecture, de synchronisation audio, d’une barre de recherche ou d’une intégration UI, une autre conception s’impose — mais pour l’usage « je veux une frame », celle-ci est très facile à suivre.

3. Les tableaux à consulter d’abord

3.1. Le déroulement du traitement

Étape API utilisée Rôle
Ouvrir le MP4 MFCreateSourceReaderFromURL Créer une source multimédia à partir du fichier
Sélectionner uniquement la vidéo SetStreamSelection Éviter de lire l’audio
Convertir en RGB32 SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING Obtenir des frames non compressées faciles à enregistrer
Se déplacer vers l’instant cible SetCurrentPosition Effectuer un seek en unités de 100 ns
Lire les frames ReadSample Récupérer les échantillons décodés un par un
Comparer avant / après timestamp de l’échantillon Déterminer la frame la plus proche de l’instant cible
Enregistrer en PNG WIC Écrire le fichier image

3.2. La règle de sélection retenue ici

Même en parlant d’« image fixe à un instant donné », une vidéo n’est pas une grandeur continue mais une suite de frames discrètes ; il est donc plus simple de fixer d’abord la règle de sélection d’une frame.

Voici la règle retenue ici :

  • Avancer avec ReadSample après le seek
  • Conserver le dernier échantillon tel que timestamp < target
  • Dès qu’arrive le premier échantillon tel que timestamp >= target, comparer l’écart entre l’échantillon précédent et l’échantillon courant
  • Adopter celui qui est le plus proche de la cible

On obtient ainsi plus facilement la frame la plus proche de la cible, plutôt que « la première frame à partir de la cible ».

3.3. La forme générale du traitement

Du début à la fin, le déroulement est à peu près : input.mp4 -> créer le Source Reader -> demander du RGB32 -> effectuer le seek -> répéter ReadSample -> comparer autour de la cible -> reconditionner en BGRA top-down -> enregistrer en PNG via WIC.

Cela paraît simple, mais la précision du seek, les échantillons nuls, le stride et le traitement du 4e octet cachent chacun un petit piège. Si l’on évite d’y tomber, c’est réellement facile à utiliser.

4. Les pièges à connaître à l’avance

4.1. SetCurrentPosition n’est pas un seek exact

Comme l’indique Microsoft Learn pour IMFSourceReader::SetCurrentPosition, cette méthode ne garantit pas un seek exact. Pour la vidéo, elle se positionne normalement juste avant la position demandée, avec un biais vers une key frame. On part ensuite du principe qu’on avance vers la position visée avec ReadSample.

Ce qui rend ce type d’implémentation assez fragile :

  • SetCurrentPosition(target)
  • Un seul ReadSample
  • Enregistrer cette frame

Sur les vidéos à GOP longs, cela dérive de façon parfaitement banale. C’est là que se cache le bourbier discret.

4.2. ReadSample peut réussir avec pSample == nullptr

ReadSample peut renvoyer S_OK tout en laissant ppSample à NULL. En fin de flux, c’est l’indicateur MF_SOURCE_READERF_ENDOFSTREAM qui est renvoyé ; pour un trou dans le flux, c’est MF_SOURCE_READERF_STREAMTICK, etc.

Ne regarder que le HRESULT et déréférencer aussitôt pSample est dangereux. Le plus sûr est de vérifier les trois éléments à la fois : HRESULT, flags et pSample.

4.3. Traiter stride et l’orientation à la légère déforme l’image

Un buffer image n’est pas forcément empaqueté en une seule ligne continue de width * bytesPerPixel. Du padding peut apparaître en fin de ligne, et les formats RGB peuvent être bottom-up. Les pages Microsoft Learn Image Stride et Uncompressed Video Buffers le précisent d’ailleurs très clairement.

Deux points sont particulièrement importants :

  • IMF2DBuffer::Lock2D renvoie le pointeur de début de la scan line 0 et le stride réel
  • pour une image bottom-up, le stride peut être négatif

Cet article reprend l’approche des helpers de Microsoft Learn et reconditionne finalement tout en un buffer BGRA contigu et top-down avant de le transmettre au PNG. En réglant ce point en amont, le côté enregistrement devient nettement plus simple.

4.4. Ne pas présumer que le 4e octet de MFVideoFormat_RGB32 est l’alpha

Malgré ce que son nom suggère, MFVideoFormat_RGB32 n’est pas un « joli RGBA » qu’on peut transmettre tel quel au PNG. En RGB 32 bits sous Windows, les octets 0, 1 et 2 sont B, G, R, et l’octet 3 peut être soit l’alpha, soit une valeur ignorée. Le point essentiel est que ce n’est pas de l’ARGB32.

Si l’on part du principe que c’est du GUID_WICPixelFormat32bppBGRA et qu’on l’enregistre tel quel, il arrive que le 4e octet contienne 0 et que l’image devienne étrangement transparente. On adopte ici la politique consistant à remplir l’alpha avec 0xFF avant l’enregistrement pour obtenir une opacité totale.

5. Le déroulement de l’implémentation

5.1. Créer le Source Reader en mode synchrone

Puisqu’il suffit ici de récupérer une seule frame, on choisit le ReadSample synchrone plutôt que des callbacks asynchrones. En mode synchrone, ReadSample bloque jusqu’au prochain échantillon, mais pour une extraction ponctuelle d’une image fixe, l’implémentation reste très directe.

À la création du reader, on effectue ces quatre opérations :

  • MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  • Désactiver d’abord tous les streams
  • N’activer que MF_SOURCE_READER_FIRST_VIDEO_STREAM
  • Définir le type de sortie sur MFMediaType_Video / MFVideoFormat_RGB32

La suite du code peut ainsi partir du principe qu’« on reçoit des frames en RGB32 ».

5.2. Après le seek, resserrer progressivement en surveillant le timestamp

Après SetCurrentPosition, on n’enregistre pas immédiatement. On lit les échantillons avec ReadSample en comparant la dernière frame avant la cible et la première frame qui la franchit.

Cette étape supplémentaire absorbe l’essentiel de l’imprécision du seek.

5.3. Reconditionner l’échantillon en BGRA top-down

L’échantillon extrait n’est pas écrit directement en PNG : on le reconditionne d’abord dans un buffer BGRA top-down.

  • Le regrouper en un seul buffer avec ConvertToContiguousBuffer
  • Obtenir la scan line 0 et le stride réel via le helper BufferLock
  • Copier ligne par ligne dans le buffer top-down
  • Mettre l’alpha à 0xFF

Le côté enregistrement peut ainsi le traiter comme « une simple image BGRA 32 bpp ».

5.4. Laisser WIC gérer l’enregistrement en PNG

L’enregistrement utilise IWICBitmapEncoder / IWICBitmapFrameEncode de WIC. La répartition est simple : Media Foundation récupère la frame, WIC la transforme en image. Le tout reste entièrement dans les API standard de Windows.

6. Une checklist pour la pratique

Élément Ce qu’il faut vérifier Ce qui arrive souvent si on l’oublie
Précision du seek Ne pas se contenter d’une seule lecture juste après SetCurrentPosition Enregistrer une frame bien avant l’instant demandé
Échantillons NULL Vérifier HRESULT, flags et pSample — les trois Déréférencement nul en fin de flux ou sur un stream tick
Stride Absorber le stride réel et l’orientation verticale L’image se déforme ou se retrouve à l’envers
Le 4e octet du RGB32 Fixer l’alpha à 0xFF Un PNG transparent
Plage temporelle Respecter 0 <= target < duration Comportement inattendu près de la fin du flux
Extraction répétée Répéter les seeks plutôt que recréer le reader Lenteur inutile
Nombre de copies En traitement de masse, garder à l’esprit le coût de ConvertToContiguousBuffer Surconsommation de CPU et de bande passante mémoire
Changement de format Traiter séparément les vidéos particulières dont la résolution change en cours de flux Les hypothèses de largeur/hauteur ne tiennent plus

7. Notes de compilation et d’exécution

Le code à la fin de cet article est conçu pour être facile à ajouter sous la forme d’un unique .cpp à une application console C++ de Visual Studio.

Voici quelques points utiles à connaître :

  • Des directives #pragma comment(lib, ...) sont incluses, donc aucun réglage de linker supplémentaire n’est en principe nécessaire
  • wmain est utilisé, si bien que les arguments de ligne de commande restent traités en Unicode de bout en bout
  • Pour rester facile à coller même avec le modèle par défaut d’application console qui comporte pch.h ou stdafx.h, le début du code utilise __has_include pour les récupérer automatiquement
  • Si le projet impose malgré tout son propre en-tête précompilé, il suffit de configurer « Ne pas utiliser d’en-têtes précompilés » pour ce seul .cpp pour que la compilation passe
  • La configuration x64 est recommandée

L’usage est ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>. Par exemple, on l’exécute ainsi : ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png.

8. En résumé

Pour extraire avec Media Foundation l’image fixe d’un MP4 à un instant donné, se limiter à SetCurrentPosition et ReadSample ne suffit pas tout à fait. En réalité :

  • le seek n’est pas exact
  • il vaut mieux comparer les frames avant/après en surveillant le timestamp
  • un ReadSample réussi peut malgré tout ne renvoyer aucun échantillon
  • il faut absorber le stride et l’orientation de l’image avant d’enregistrer
  • ne pas présumer que le 4e octet de RGB32 est l’alpha

En maîtrisant ces points, on réduit nettement le risque d’incidents.

Cet exemple, en tant que configuration minimale centrée sur l’extraction correcte d’une seule frame, devrait être très facile à utiliser. Il se transpose directement à la génération de miniatures, à l’enregistrement de la frame représentative d’une vidéo de surveillance, ou à la production de preuves pour des journaux d’inspection.

9. Références

10. Le code complet, prêt à coller dans un .cpp

Le bloc unique ci-dessous est prévu pour être repris tel quel dans un projet d’application console C++ Visual Studio. Les arguments de ligne de commande sont, dans l’ordre, input.mp4, seconds et output.png. La structure tient en un seul fichier autonome, ce qui la rend facile à coller dans un projet.

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

        // Le 4e octet de MFVideoFormat_RGB32 n'est pas forcément l'alpha,
        // donc on le force à l'opacité avant l'enregistrement en 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;
}

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Pourquoi IMFSourceReader::SetCurrentPosition ne se positionne-t-il pas exactement à l'instant demandé ?
SetCurrentPosition ne garantit pas un seek exact. Pour la vidéo, il se positionne normalement juste avant la position demandée, avec un biais vers une key frame, l'idée étant qu'on avance ensuite vers la cible avec ReadSample. Sur les vidéos à GOP longs, enregistrer la toute première frame après le seek dérive systématiquement de la cible. L'approche stable consiste à continuer de lire des échantillons, à conserver la dernière frame avant l'instant cible, puis, quand la première frame à l'instant cible ou après arrive, à comparer les deux horodatages et à adopter celle qui est la plus proche.
Pourquoi ReadSample renvoie-t-il S_OK tout en me donnant un échantillon nul ?
ReadSample peut réussir alors que ppSample vaut NULL. En fin de flux, il renvoie l'indicateur MF_SOURCE_READERF_ENDOFSTREAM, et pour les interruptions de flux, MF_SOURCE_READERF_STREAMTICK, dans les deux cas sans échantillon. Se contenter de vérifier le HRESULT et déréférencer immédiatement le pointeur d'échantillon est dangereux : le réflexe sûr consiste à regarder les trois éléments à la fois, le HRESULT, les flags et le pointeur d'échantillon.
Pourquoi mon PNG extrait est-il transparent ou à l'envers ?
Deux pièges distincts en sont la cause. La transparence vient du quatrième octet de MFVideoFormat_RGB32 : il n'est pas forcément l'alpha, donc si vous enregistrez le buffer en BGRA 32 bits par pixel sans le corriger, des zéros dans cet octet produisent une image étrangement transparente — fixez l'alpha à 0xFF avant l'enregistrement pour que l'image soit opaque. Les images à l'envers ou déformées viennent du stride et de l'orientation : les lignes peuvent contenir du padding et les formats RGB peuvent être bottom-up avec un stride négatif, donc il faut tout reconditionner dans un buffer BGRA contigu et top-down avant de le transmettre à l'encodeur PNG.
Faut-il des bibliothèques externes pour extraire une frame d'un MP4 sous Windows ?
Non. L'approche de cet article n'utilise que les API standard de Windows : IMFSourceReader en mode synchrone pour ouvrir le MP4, sélectionner le flux vidéo, convertir la sortie en RGB32 via MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, effectuer le seek et lire les frames, puis WIC (IWICBitmapEncoder) pour écrire le PNG. Pour une extraction ponctuelle d'une seule image fixe, le Source Reader synchrone est une porte d'entrée plus naturelle que la Media Session, et l'article se termine par un unique listing C++ autonome que vous pouvez coller dans une application console Visual Studio.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog