Extraire une image fixe d'un MP4 à un instant précis avec Media Foundation
· Mis à jour le: · Go Komura · 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 Readerest ici un point d’entrée plus direct que laMedia Session IMFSourceReader::SetCurrentPositionne 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 avecReadSampleet comparer les instants situés avant et après l’heure viséeReadSamplepeut réussir tout en renvoyantpSample == nullptr. Il faut vérifierflagsetpSample, pas seulement leHRESULT- Orienter le type de média de sortie vers
MFVideoFormat_RGB32facilite l’enregistrement - Cependant, le 4e octet du
RGB32n’est pas forcément l’alpha : l’écrire tel quel en PNG peut produire une image transparente. Le plus sûr est d’y mettre0xFFavant l’enregistrement pour la rendre opaque - Traiter à la légère le
stridepar 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
IMFSourceReaderen 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
ReadSampleaprè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::Lock2Drenvoie 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 wmainest 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.houstdafx.h, le début du code utilise__has_includepour 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
.cpppour 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
ReadSampleréussi peut malgré tout ne renvoyer aucun échantillon - il faut absorber le
strideet l’orientation de l’image avant d’enregistrer - ne pas présumer que le 4e octet de
RGB32est 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
- Code source complet de l’exemple de cet article : media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)
- Microsoft Learn : Using the Source Reader to Process Media Data
- Microsoft Learn :
IMFSourceReader::SetCurrentPosition - Microsoft Learn :
IMFSourceReader::ReadSample - Microsoft Learn :
IMFSourceReader::SetCurrentMediaType - Microsoft Learn :
IMF2DBuffer - Microsoft Learn :
IMF2DBuffer::Lock2D - Microsoft Learn : Uncompressed Video Buffers
- Microsoft Learn : Image Stride
- Microsoft Learn : MF_MT_FRAME_SIZE attribute
- Microsoft Learn : MF_MT_DEFAULT_STRIDE attribute
- Microsoft Learn : Native pixel formats overview (WIC)
- Microsoft Learn : Uncompressed RGB Video Subtypes
10. 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,
¤tTimestamp,
&pCurrent);
if (FAILED(hr))
{
SafeRelease(&pCurrent);
break;
}
if ((flags & MF_SOURCE_READERF_ENDOFSTREAM) != 0)
{
SafeRelease(&pCurrent);
if (hasBefore)
{
*ppSample = pBefore;
pBefore = nullptr;
if (pChosenTimestampHns != nullptr)
{
*pChosenTimestampHns = beforeTimestamp;
}
hr = S_OK;
}
else
{
hr = MF_E_END_OF_STREAM;
}
break;
}
if ((flags & MF_SOURCE_READERF_STREAMTICK) != 0)
{
SafeRelease(&pCurrent);
continue;
}
if (pCurrent == nullptr)
{
continue;
}
if (currentTimestamp < targetHns)
{
SafeRelease(&pBefore);
pBefore = pCurrent;
pCurrent = nullptr;
beforeTimestamp = currentTimestamp;
hasBefore = true;
continue;
}
if (hasBefore)
{
diffBefore = targetHns - beforeTimestamp;
diffCurrent = currentTimestamp - targetHns;
if (diffBefore <= diffCurrent)
{
*ppSample = pBefore;
pBefore = nullptr;
if (pChosenTimestampHns != nullptr)
{
*pChosenTimestampHns = beforeTimestamp;
}
SafeRelease(&pCurrent);
}
else
{
*ppSample = pCurrent;
pCurrent = nullptr;
if (pChosenTimestampHns != nullptr)
{
*pChosenTimestampHns = currentTimestamp;
}
}
}
else
{
*ppSample = pCurrent;
pCurrent = nullptr;
if (pChosenTimestampHns != nullptr)
{
*pChosenTimestampHns = currentTimestamp;
}
}
hr = S_OK;
break;
}
SafeRelease(&pBefore);
return hr;
}
HRESULT CopyContiguousBufferToTopDownBgra(
IMFMediaBuffer* pBuffer,
LONG defaultStride,
UINT32 width,
UINT32 height,
std::vector<BYTE>& pixels,
UINT32* pStride)
{
if (pBuffer == nullptr || pStride == nullptr)
{
return E_POINTER;
}
BufferLock lock(pBuffer);
BYTE* pScanLine0 = nullptr;
LONG actualStride = 0;
HRESULT hr = lock.LockBuffer(defaultStride, height, &pScanLine0, &actualStride);
if (FAILED(hr))
{
return hr;
}
if (width > (std::numeric_limits<UINT32>::max() / 4))
{
return E_INVALIDARG;
}
const UINT32 destStride = width * 4;
const LONG actualStrideAbs = (actualStride < 0) ? -actualStride : actualStride;
if (actualStrideAbs < static_cast<LONG>(destStride))
{
return E_UNEXPECTED;
}
pixels.resize(static_cast<size_t>(destStride) * height);
BYTE* pDestRow = pixels.data();
BYTE* pSrcRow = pScanLine0;
for (UINT32 y = 0; y < height; ++y)
{
std::memcpy(pDestRow, pSrcRow, destStride);
// 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 associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Comment incruster une image et du texte dans les images d'une vidéo MP4 avec Media Foundation
Comment incruster une image et du texte dans chaque image d'une vidéo MP4 avec Media Foundation pour produire un nouveau MP4, en structur...
Comment convertir du YUV en RGB avec Media Foundation
Comment convertir des trames YUV en RGB avec Media Foundation : la conversion automatique du Source Reader vers RGB32, ou la conversion m...
Introduction à Media Foundation - Comprendre l'API à travers le prisme COM
Nous expliquons ce qu'est Media Foundation, ainsi que le vocabulaire de base des API multimédias Windows - COM, HRESULT, IMFSourceReader,...
Pièges de la mémoire partagée et bonnes pratiques concrètes
Nous passons en revue les pièges de la mémoire partagée en usage réel, ainsi que la conception qui réduit le taux d'incidents : synchroni...
Appeler une DLL C# Native AOT depuis C/C++
Nous faisons le point sur la publication d'une bibliothèque de classes C# en DLL native avec Native AOT, et sur l'appel depuis C/C++ des ...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Il s'agit d'extraire une image fixe d'une vidéo à l'aide de Media Foundation, du Source Reader et de WIC, un thème d'implémentation proche du développement d'applications Windows.
Conseil technique et revue de conception
Si vous souhaitez clarifier avant l'implémentation la précision du seek, le format du buffer, le stride ou l'orientation de l'image, nous pouvons intervenir dès la définition des orientations dans le cadre du conseil technique et de la revue de conception.
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.
Liens publics