Estrazione di un'immagine fissa da un MP4 in un momento specifico con Media Foundation
· Aggiornato il: · Go Komura · 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 aMedia Sessionper questo lavoro IMFSourceReader::SetCurrentPositionnon garantisce una ricerca esatta. Normalmente arriva leggermente prima del target, inclinato verso un fotogramma chiave, quindi è necessario avanzare conReadSamplee confrontare i fotogrammi su entrambi i lati del tempo targetReadSamplepuò avere successo e darti comunquepSample == nullptr. ControllaflagsepSample, non soloHRESULT- Indirizzare l’uscita media type a
MFVideoFormat_RGB32semplifica il risparmio - Tuttavia, il quarto byte di
RGB32non è necessariamente alfa, quindi scriverlo direttamente su PNG può produrre un’immagine trasparente. È più sicuro impostarlo su0xFFprima di salvare in modo che l’immagine sia opaca - Gestisci con noncuranza l’orientamento per riga
stridee 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
CoInitializeExe rilasciare i puntatori alle interfacce conRelease(o l’equivalente C#). - Il valore restituito
HRESULTsi giudica con le macroSUCCEEDED/FAILED. In caso di fallimento, leggere il valore esadecimale così com’è. I valori che iniziano con0x8007provengono da errori Win32; i 16 bit inferiori sono il codice di errore Win32 (ad esempio0x80070057è lo stesso valore diE_INVALIDARG). Gli errori specifici di Media Foundation che iniziano conMF_E_sono definiti inmferror.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
IMFSourceReaderin 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
ReadSampledopo la ricerca - Mantieni l’ultimo campione con
timestamp < target - When the first sample with
timestamp >= targetarrivato, 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::Lock2Drestituisce 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.hostdafx.h, l’inizio del codice usa__has_includeper 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
.cppe 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.
8. Riepilogo
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
ReadSampleriuscito potrebbe comunque non trasportare alcun campione - Assorbire
stridee l’orientamento dell’immagine prima di salvare - Non dare per scontato che il quarto byte di
RGB32sia 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
- Codice di esempio completo per questo articolo: media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)
- Microsoft Learn: Utilizzo di Source Reader per elaborare i dati multimediali
- 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: Passo dell’immagine
- Microsoft Learn: Attributo MF_MT_FRAME_SIZE
- Microsoft Learn: Attributo MF_MT_DEFAULT_STRIDE
- Microsoft Learn: Panoramica dei formati pixel nativi (WIC)
- Microsoft Learn: Sottotipi video RGB non compressi
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,
¤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);
// 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 correlati
Articoli recenti con gli stessi tag per approfondire argomenti vicini.
Come convertire YUV in RGB con Media Foundation
Due modi per ottenere RGB da YUV in Media Foundation: lasciare che Source Reader emetta RGB32 o convertire NV12 / YUY2 con il passo e la ...
Un'introduzione a Media Foundation: comprendere l'API attraverso una lente COM
Spieghiamo cos'è Media Foundation, insieme al vocabolario di base di Windows media API - COM, HRESULT, IMFSourceReader, MFTs - nell'ordin...
Chiamando un C# Native AOT DLL da C / C++
Esporta i punti di ingresso C da C# con Native AOT e UnmanagedCallersOnly e chiamali da C / C++: impostazione del progetto, gestione dell...
Icone nella system tray e notifiche toast nelle app Windows — le insidie di NotifyIcon e come scegliere l'AppNotification giusta
Una guida pratica per mantenere un'applicazione Windows aziendale residente nella system tray (area di notifica) e avvisare l'utente tram...
Fino a quando funzioneranno le applicazioni VB6? — Lo stato del supporto al runtime e un percorso pratico verso la migrazione a .NET
Fino a quando continueranno a funzionare le applicazioni VB6? Questo articolo chiarisce l'asimmetria tra la politica di supporto del runt...
Argomenti correlati
Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.
Argomenti tecnici Windows
Portale su sviluppo Windows, analisi dei problemi e valorizzazione delle risorse esistenti.
Servizi collegati all’argomento
L’articolo è direttamente collegato ai servizi seguenti.
Sviluppo di applicazioni Windows
L'estrazione di immagini fisse dal video utilizzando Media Foundation, Source Reader e WIC è un classico argomento di implementazione dello sviluppo di applicazioni Windows.
Consulenza tecnica e revisione del progetto
Se desideri risolvere la questione della precisione, dei formati buffer, del passo e dell'orientamento dell'immagine prima dell'implementazione, possiamo iniziare con l'impostazione della direzione come consulenza tecnica e revisione del progetto.
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.