Cómo extraer una imagen fija de un MP4 en un instante específico con Media Foundation
· Actualizado el: · Go Komura · Media Foundation, C++, Desarrollo en Windows, WIC
Extraer «un fotograma en el segundo 12,3» de un MP4 es un requisito bastante habitual. Generación de miniaturas, registros de inspección, fotogramas representativos de vídeo de vigilancia, evidencias en registros de equipos, entre otros casos.
Sin embargo, en Media Foundation esto no es del todo directo. Parece que basta con llamar una vez a ReadSample después de SetCurrentPosition, pero en la práctica intervienen el key frame, el timestamp, el stride, la orientación vertical de la imagen y el 4.º byte de RGB32. Si se avanza sin cuidado, ocurren pequeños percances molestos: el instante queda ligeramente desplazado, la imagen aparece al revés o el PNG resulta extrañamente transparente.
El panorama general de Media Foundation en sí lo tratamos antes en Introducción a Media Foundation: entender la API desde la perspectiva de COM, que también puede servir de referencia. Esta vez bajamos un nivel desde ahí y nos centramos únicamente en extraer un fotograma de un MP4.
En este artículo, usando IMFSourceReader, organizamos el procedimiento hasta extraer un único fotograma lo más cercano posible al instante indicado y guardarlo como PNG, incluyendo las trampas habituales en el trabajo real. Y al final dejamos un código en un solo archivo, listo para pegar tal cual en el .cpp de un proyecto de aplicación de consola en C++ de Visual Studio. No hay fragmentos de código sueltos a lo largo del artículo: basta con llevarse el último bloque para que funcione.
Además, el código que aparece en este artículo está publicado en GitHub como conjunto de muestras (una aplicación de consola en C++ contenida en un solo archivo).
media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)
1. Primero, la conclusión
Adelantamos únicamente la conclusión:
- Si lo que se busca es extraer un solo fotograma de un MP4,
Source Readeres, en este caso, un punto de entrada más directo queMedia Session IMFSourceReader::SetCurrentPositionno garantiza un seek exacto. Normalmente se desplaza un poco antes del target, en especial hacia el lado del key frame, así que después es necesario avanzar conReadSampley comparar los instantes anterior y posterior al momento buscadoReadSamplepuede tener éxito y aun así devolverpSample == nullptr. Hay que observar no solo elHRESULT, sino tambiénflagsypSample- Fijar el tipo de medio de salida en
MFVideoFormat_RGB32facilita el guardado - Sin embargo, el 4.º byte de
RGB32no siempre es el alpha, así que escribirlo directamente en un PNG puede producir una imagen transparente. Lo más seguro es escribir0xFFantes de guardar para dejarla opaca - Si se maneja con descuido el
stridede cada fila y la orientación top-down / bottom-up, la imagen se deforma, así que el sample obtenido se reordena una vez en un BGRA continuo y top-down antes de pasarlo al PNG
En resumen, seek -> leer una vez -> guardar es un poco descuidado. Llegando hasta seek -> comparar el instante anterior y posterior observando el timestamp -> copiar teniendo en cuenta el stride -> guardar como PNG, el resultado es bastante más estable.
2. Supuestos de este artículo
2.1. Público objetivo y conocimientos previos
- Se asume que el lector ha llamado a las API COM de Windows desde C++. Basta con entender el flujo de inicializar con
CoInitializeExy liberar los punteros a interfaz conRelease - El valor de retorno
HRESULTse evalúa con las macrosSUCCEEDED/FAILED. Cuando falla, se lee el valor hexadecimal tal cual. Los valores que empiezan por0x8007provienen de un error de Win32, donde los 16 bits inferiores son el código de error de Win32 (0x80070057equivale aE_INVALIDARG). Los errores propios de Media Foundation que empiezan porMF_E_están definidos enmferror.h - El artículo está pensado para poder leerse aunque sea la primera vez que se trabaja con Media Foundation. El panorama general desde la perspectiva de COM está en Introducción a Media Foundation: entender la API desde la perspectiva de COM
2.2. Entorno de desarrollo
| Elemento | Supuesto de este artículo |
|---|---|
| SO | Windows 10 / Windows 11 |
| IDE | Visual Studio 2022 (carga de trabajo «Desarrollo de escritorio con C++») |
| SDK | Windows SDK incluido con Visual Studio (incluye las cabeceras y bibliotecas de Media Foundation y WIC) |
| Proyecto | Plantilla «Aplicación de consola» de C++ |
| Plataforma | x64 |
| Bibliotecas adicionales | Ninguna. Los enlaces a bibliotecas como mfplat.lib se resuelven con #pragma comment(lib, ...) dentro del propio código |
Los detalles sobre cómo pegar el código y las precauciones con los encabezados precompilados están reunidos en «7. Notas sobre la compilación y la ejecución».
2.3. Supuestos de entrada y salida
Los supuestos de este artículo son los siguientes:
- La entrada es un archivo MP4 local
- Lo que se necesita es una única imagen fija
- No se busca «exactamente el instante indicado», sino «el fotograma más cercano al instante indicado»
- La implementación usa
IMFSourceReaderen modo síncrono - El formato de guardado es PNG mediante WIC
- No se usan bibliotecas externas; todo se resuelve con las API estándar de Windows
- Se asume un MP4 habitual en el que la resolución no cambia a mitad de vídeo
Si se necesita reproducción, sincronización de audio, barra de progreso o integración con la interfaz de usuario, hace falta otro diseño, pero para el caso de querer extraer un fotograma, este enfoque resulta bastante claro.
3. Tablas de referencia rápida
3.1. Flujo del procesamiento
| Qué se hace | API que se usa | Función |
|---|---|---|
| Abrir el MP4 | MFCreateSourceReaderFromURL |
Crear el origen de medios a partir del archivo |
| Seleccionar solo el vídeo | SetStreamSelection |
Evitar leer el audio |
| Convertir a RGB32 | SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING |
Obtener un fotograma sin comprimir, fácil de guardar |
| Moverse al instante indicado | SetCurrentPosition |
Hacer seek en unidades de 100 ns |
| Leer el fotograma | ReadSample |
Obtener las muestras decodificadas una a una |
| Comparar el instante anterior y posterior | timestamp del sample | Determinar el fotograma más cercano al instante indicado |
| Guardar como PNG | WIC | Escribir el archivo de imagen |
3.2. Regla de selección en este artículo
Aunque se hable de «la imagen fija de un instante indicado», el vídeo no es una magnitud continua sino fotogramas discretos, así que conviene decidir de antemano con qué regla se elige uno solo.
En este artículo usamos la siguiente regla:
- Avanzar con
ReadSampledespués del seek - Conservar el último sample cuyo
timestamp < target - Cuando llega el primer sample con
timestamp >= target, comparar la diferencia entre el sample anterior y el sample actual - Adoptar el que quede más cerca del target
Esto permite obtener no «el primer fotograma a partir del target», sino el fotograma más cercano al target.
3.3. Panorama del procesamiento
De principio a fin, el procesamiento es más o menos así: input.mp4 -> crear el Source Reader -> solicitar RGB32 -> hacer seek -> repetir ReadSample -> comparar el instante anterior y posterior al target -> reordenar en BGRA top-down -> guardar como PNG con WIC.
A simple vista parece sencillo, pero hay pequeñas trampas en la precisión del seek, en que el sample puede ser nulo, en el stride y en el tratamiento del 4.º byte. Si se evitan esas cuatro, la implementación en sí queda bastante ordenada.
4. Trampas que conviene conocer de antemano
Antes de entrar en este capítulo, resumimos brevemente los términos que van a aparecer.
| Término | Significado |
|---|---|
| key frame (fotograma clave) | Un fotograma que se puede decodificar por sí solo, sin referirse a los fotogramas anteriores o posteriores. En H.264 corresponden, por ejemplo, a las imágenes IDR |
| GOP (Group of Pictures) | El conjunto de fotogramas desde un key frame hasta justo antes del siguiente key frame. Cuanto más largo es el GOP, mayor es el espacio entre key frames, y más lejos del instante indicado tiende a quedar la posición tras el seek |
| stride | La cantidad de bytes que ocupa una fila en el búfer de imagen. No siempre coincide con ancho × bytes por píxel, ya que puede haber padding al final de la fila |
| top-down / bottom-up | La diferencia entre si el inicio del búfer de imagen corresponde a la fila superior o a la inferior. El bottom-up se representa con un stride de valor negativo |
MF_SOURCE_READERF_STREAMTICK |
Uno de los flags que devuelve ReadSample, e indica que hay un hueco (una interrupción de datos) en la transmisión. En la llamada donde se activa este flag no se obtiene un fotograma, así que hay que volver a leer |
4.1. SetCurrentPosition no es un seek exacto
Como se indica en IMFSourceReader::SetCurrentPosition de Microsoft Learn, esta función no garantiza un seek exacto. En los vídeos, normalmente se desplaza un poco antes de la posición indicada, en especial hacia el lado del key frame. Además, se parte de la base de que después hay que avanzar con ReadSample hasta llegar a la posición deseada.
Por eso, una implementación como la siguiente resulta bastante arriesgada:
SetCurrentPosition(target)- Una llamada a
ReadSample - Guardar ese fotograma
En vídeos con un GOP largo, esto se desvía. Si el intervalo entre key frames es de 2 segundos, se puede terminar guardando un fotograma hasta 2 segundos antes del instante indicado.
4.2. ReadSample puede tener éxito y aun así devolver pSample == nullptr
ReadSample puede devolver S_OK y aun así ppSample puede ser NULL. Si se ha llegado al final se devuelve el flag MF_SOURCE_READERF_ENDOFSTREAM, y si hay un hueco en la transmisión, MF_SOURCE_READERF_STREAMTICK, entre otros.
Es peligroso mirar solo el HRESULT y referenciar pSample de inmediato. Lo más seguro es observar el HRESULT, los flags y el pSample, los tres juntos.
4.3. Tratar el stride y la orientación vertical de forma descuidada deforma la imagen
El búfer de imagen no siempre está compactado de forma continua como ancho × bytes por píxel. Puede haber padding al final de cada fila, y los formatos RGB a veces son bottom-up. En Image Stride y en Uncompressed Video Buffers de Microsoft Learn este punto también está descrito con bastante claridad.
Hay dos puntos especialmente importantes:
IMF2DBuffer::Lock2Ddevuelve el puntero al inicio de la scan line 0 y el stride real- En una imagen bottom-up, el stride puede ser negativo
En este artículo adoptamos la idea del helper de Microsoft Learn y, al final, reordenamos todo en un búfer BGRA continuo y top-down antes de pasarlo al PNG. Dejar esto resuelto de antemano simplifica bastante el lado del guardado.
4.4. No dar por hecho que el 4.º byte de MFVideoFormat_RGB32 es el alpha
MFVideoFormat_RGB32, a pesar de lo que sugiere su nombre, no es un «RGBA limpio» que se pueda pasar tal cual a un PNG. En el RGB de 32 bits de Windows, los bytes 0, 1 y 2 son B, G y R, y el byte 3 puede ser el alpha o puede ignorarse. Lo importante es que no se trata de ARGB32.
Si se asume que es GUID_WICPixelFormat32bppBGRA y se guarda tal cual, puede ocurrir que el 4.º byte contenga un 0 y la imagen quede extrañamente transparente. En este artículo adoptamos el criterio de rellenar el alpha con 0xFF antes de guardar, para que quede totalmente opaca.
5. Flujo de la implementación
5.1. Crear el Source Reader en modo síncrono
Como esta vez basta con obtener un solo fotograma, en lugar de un callback asíncrono usamos ReadSample síncrono. En modo síncrono, ReadSample bloquea hasta obtener el siguiente sample, pero para una extracción puntual de una sola imagen fija la implementación resulta bastante directa.
Al crear el Reader hay que hacer estas cuatro cosas:
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE- Desactivar de momento todos los streams
- Activar solo
MF_SOURCE_READER_FIRST_VIDEO_STREAM - Establecer el tipo de salida en
MFMediaType_Video/MFVideoFormat_RGB32
Con esto, en el resto del código se puede escribir partiendo de la base de que «se recibe un fotograma RGB32».
5.2. Después del seek, ajustar observando el timestamp
Tras SetCurrentPosition, no se guarda de inmediato. Mientras se leen samples con ReadSample, se compara el último fotograma anterior al target con el primero que lo sobrepasa.
Este paso adicional permite absorber bastante bien la imprecisión del seek.
5.3. Convertir el sample en un BGRA top-down
El sample obtenido no se escribe directamente en el PNG: primero se reordena en un búfer BGRA top-down.
- Convertirlo en un único búfer con
ConvertToContiguousBuffer - Obtener la scan line 0 y el stride real con el helper
BufferLock - Copiar fila a fila al búfer top-down
- Fijar el alpha en
0xFF
Con esto, el lado del guardado puede tratarlo como una simple «imagen BGRA de 32 bpp».
5.4. Delegar el guardado en PNG a WIC
El guardado se realiza con IWICBitmapEncoder / IWICBitmapFrameEncode de WIC. El reparto de tareas es: Media Foundation obtiene el fotograma y WIC lo convierte en imagen. Esta parte se completa solo con las API estándar de Windows.
6. Lista de verificación para el trabajo real
| Elemento | Qué revisar | Qué suele pasar si se pasa por alto |
|---|---|---|
| Precisión del seek | No decidir con una sola llamada justo después de SetCurrentPosition |
Guardar un fotograma bastante anterior al instante indicado |
| Sample nulo | Revisar HRESULT, flags y pSample en conjunto |
Una desreferencia nula al llegar al final o ante un stream tick |
| Stride | Absorber el stride real y la orientación vertical | La imagen se deforma o queda al revés |
| 4.º byte de RGB32 | Fijar el alpha en 0xFF |
El PNG queda transparente |
| Rango de tiempo | Respetar 0 <= target < duration |
Comportamiento no deseado cerca del final |
| Extracción continua | Repetir el seek sin recrear el Reader | Lentitud innecesaria |
| Número de copias | Tener en cuenta el coste de ConvertToContiguousBuffer en procesos masivos |
Uso excesivo de CPU y de ancho de banda de memoria |
| Cambios de formato | Usar otro diseño para vídeos especiales cuya resolución cambia a mitad de reproducción | Los supuestos de ancho y alto dejan de ser válidos |
De estas filas, «extracción continua» y «número de copias» son puntos que no afectan a la extracción de un solo fotograma en sí, sino que cobran importancia cuando se amplía el caso a extraer decenas de fotogramas del mismo vídeo. La muestra de este artículo se limita a extraer uno y terminar, así que, si necesita extraer varios fotogramas, cambie el enfoque para repetir SetCurrentPosition y ReadSample sin recrear el Source Reader. El mapa completo de Media Foundation, incluidas otras opciones además de Source Reader (como Media Session), está en Introducción a Media Foundation: entender la API desde la perspectiva de COM.
7. Notas sobre la compilación y la ejecución
El código final de este artículo está preparado de forma que se pueda añadir fácilmente como un único .cpp a una aplicación de consola en C++ de Visual Studio. Los supuestos sobre el propio entorno están reunidos en 2.2.
7.1. Notas sobre la compilación
Conviene tener presentes estos puntos:
- Se incluye
#pragma comment(lib, ...), así que básicamente no hace falta ninguna configuración adicional del enlazador - Se usa
wmain, de modo que los argumentos de línea de comandos se manejan en Unicode sin conversiones - Para que sea fácil de pegar incluso cuando la plantilla predeterminada de aplicación de consola tiene
pch.hostdafx.h, al principio del código se usa__has_includepara detectarlos - Aun así, si el proyecto obliga a usar su propio encabezado precompilado, basta con poner este
.cppen «No usar encabezados precompilados» para que compile - Se recomienda la configuración de ejecución x64
7.2. Cómo ejecutarlo y qué aparece cuando tiene éxito
El uso es ExtractFrameFromMp4.exe <input.mp4> <segundos> <output.png>. Por ejemplo, se ejecuta como ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png.
Si tiene éxito, al final de wmain se imprimen estas tres líneas en la salida estándar:
Saved: C:\work\frame.png
Requested: 12.345 sec
Actual: (instante de presentación del fotograma adoptado) sec
Requested es directamente el número de segundos pasado como argumento, y Actual es el timestamp del fotograma realmente adoptado. Como la regla de 3.2 elige el más cercano entre el fotograma anterior y el posterior, la diferencia entre ambos valores queda, aproximadamente, dentro de la mitad del intervalo entre fotogramas (a 29,97 fps, el límite orientativo ronda los 17 ms). Si esta diferencia supera varios cientos de milisegundos, sospeche que, por ejemplo, solo se está leyendo una vez tras el seek y que la comparación del instante anterior y posterior de 4.1 no está funcionando.
Los objetivos de la comprobación de funcionamiento son estos tres:
- El código de salida es 0 y la línea
Saved:muestra la ruta de salida indicada - Al abrir el PNG resultante, la escena de ese instante se muestra con la orientación correcta (no está al revés)
- El ancho y el alto del PNG coinciden con la resolución del vídeo original, y el fondo no es transparente (indica que el relleno de alpha de 4.4 está funcionando)
Si falla, en la salida de error estándar aparece Failed. HRESULT = 0x......... El valor se lee siguiendo el criterio de 2.1. Cuando el número de segundos indicado es igual o mayor que la duración del vídeo, el resultado es 0x80070057 (E_INVALIDARG).
8. Resumen
Al extraer de un MP4 la imagen fija de un instante indicado con Media Foundation, fijarse solo en SetCurrentPosition y ReadSample se queda un poco corto. En la práctica hay que tener en cuenta que:
- El seek no es exacto
- Conviene comparar el fotograma anterior y posterior observando el timestamp
- Aunque
ReadSampletenga éxito, puede que no haya sample - Hay que absorber el
stridey la orientación de la imagen antes de guardar - No se debe dar por hecho que el 4.º byte de
RGB32es el alpha
Teniendo presentes estos puntos, el riesgo de percances se reduce bastante.
La muestra de este artículo es una configuración mínima centrada en extraer correctamente un solo fotograma. Está preparada para poder llevarse tal cual a casos como la generación de miniaturas, el guardado del fotograma representativo de vídeo de vigilancia o la salida de evidencias en registros de inspección.
9. Referencias
- Conjunto de código de muestra de este artículo: 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. Código completo listo para pegar en el .cpp
El siguiente bloque único es el código pensado para llevarse tal cual a un proyecto de aplicación de consola en C++ de Visual Studio. Los argumentos de línea de comandos son, en este orden, input.mp4, seconds y output.png. Está pensado como una única unidad autocontenida, de modo que resulte fácil de pegar en el proyecto.
Como es un bloque largo, dejamos antes una referencia de qué función corresponde a qué parte del texto. Tanto al leerlo como al investigar por qué algo no funciona, conviene partir de esta tabla.
| Función / clase del código | Sección correspondiente | Papel y trampa que resuelve |
|---|---|---|
MediaFoundationScope |
5.1 | Agrupa la inicialización y la limpieza de CoInitializeEx y MFStartup |
CreateConfiguredSourceReader |
5.1 | Crea el Reader, selecciona solo el stream de vídeo y solicita MFVideoFormat_RGB32 |
GetPresentationDuration |
6 (rango de tiempo) | Obtiene la duración del vídeo y comprueba que se cumple 0 <= target < duration |
SeekSourceReader |
4.1 / 5.2 | Hace seek con SetCurrentPosition. Por sí solo no llega a ser exacto |
ReadNearestVideoSample |
3.2 / 4.1 / 4.2 / 5.2 | Compara el instante anterior y posterior al target y elige un fotograma. Aquí también se gestiona el nulo de flags y pSample |
GetDefaultStride |
4.3 | Calcula el stride cuando no hay MF_MT_DEFAULT_STRIDE |
BufferLock |
4.3 / 5.3 | Obtiene la scan line 0 y el stride real con IMF2DBuffer::Lock2D |
CopyContiguousBufferToTopDownBgra |
4.3 / 4.4 / 5.3 | Reordena fila a fila en el búfer top-down y rellena el 4.º byte con 0xFF |
CopySampleToTopDownBgra |
5.3 | Obtiene el tamaño del fotograma y el stride, y llama a la copia anterior |
SaveBgraToPng |
5.4 | Escribe el BGRA de 32 bpp como PNG mediante WIC |
ExtractFrameFromMp4ToPng |
5 completo | Punto de entrada que llama en orden a las funciones anteriores |
TryParseSeconds / wmain |
7.2 | Análisis de los argumentos y muestra de Requested / Actual |
#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);
// El 4.º byte de MFVideoFormat_RGB32 no siempre es el alpha,
// así que se fija como opaco antes de guardar el 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;
}
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Cómo grabar imágenes y texto en los fotogramas de un MP4 con Media Foundation
Con Media Foundation, cómo grabar imágenes y texto en cada fotograma de un MP4 repartiendo el trabajo entre Source Reader, el dibujado, l...
Cómo convertir YUV a RGB en Media Foundation
Cómo convertir fotogramas YUV a RGB en Media Foundation: conversión automática con Source Reader, conversión manual de NV12/YUY2, stride ...
Introducción a Media Foundation - Cómo entender la API desde la perspectiva de COM
Explicamos qué es Media Foundation junto con los términos básicos de su API multimedia en Windows —COM, HRESULT, IMFSourceReader, MFT— en...
Trampas de la memoria compartida y buenas prácticas para producción
Analizamos las trampas de usar memoria compartida en producción y el diseño que reduce la tasa de incidentes: sincronización, visibilidad...
Cómo invocar una DLL nativa de C# Native AOT desde C/C++
Publicar una biblioteca de C# como DLL nativa con Native AOT e invocar sus puntos UnmanagedCallersOnly desde C/C++: casos de uso, patrone...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Se trata de extraer una imagen fija de un vídeo usando Media Foundation, Source Reader y WIC, por lo que está cerca del tema de implementación de desarrollo de aplicaciones para Windows.
Consultoría técnica y revisión de diseño
Si desea ordenar de antemano la precisión del seek, el formato del búfer, el stride y la orientación de la imagen antes de implementar, podemos empezar por la consultoría técnica y la revisión de diseño para definir el enfoque.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué debo usar para extraer una imagen fija de un instante específico de un MP4?
- Si solo necesita extraer una imagen, IMFSourceReader es un punto de entrada más directo que Media Session. El flujo consiste en abrir el archivo con MFCreateSourceReaderFromURL, seleccionar únicamente el vídeo con SetStreamSelection, solicitar MFVideoFormat_RGB32, hacer seek con SetCurrentPosition, obtener el fotograma con ReadSample y guardarlo como PNG con WIC. Todo esto se puede completar solo con las API estándar de Windows, sin bibliotecas externas.
- ¿SetCurrentPosition permite moverse exactamente al instante indicado?
- No. IMFSourceReader::SetCurrentPosition no garantiza un seek exacto: en los vídeos, normalmente se desplaza un poco antes de la posición indicada, en especial hacia el lado del key frame. Es necesario avanzar con ReadSample después del seek, observar el timestamp, comparar el último sample anterior al target con el primer sample posterior al target, y adoptar el que quede más cerca. Una implementación que lee una sola vez tras el seek y guarda ese fotograma se desvía con normalidad en vídeos con un GOP largo.
- ¿Por qué el PNG guardado queda transparente?
- Porque el cuarto byte de MFVideoFormat_RGB32 no siempre es el alpha. En el RGB de 32 bits de Windows, los bytes 0, 1 y 2 son B, G y R, y el byte 3 puede ser el alpha o puede ignorarse (no es ARGB32). Si se escribe tal cual en el PNG, la imagen puede quedar transparente, así que lo más seguro es escribir 0xFF en el cuarto byte antes de guardar, para dejarla completamente opaca.
- ¿Cuál es la causa de que la imagen se deforme o quede al revés?
- El manejo del stride y de la orientación vertical. El búfer de imagen no siempre está compactado de forma continua como ancho × bytes por píxel: puede haber padding al final de cada fila, y los formatos RGB a veces son bottom-up (con stride negativo). Si se obtiene el puntero al inicio de la scan line 0 y el stride real con IMF2DBuffer::Lock2D, y se reordena todo en un único búfer BGRA continuo top-down antes de pasarlo al PNG, el guardado se simplifica y se evitan las deformaciones.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.