Media Foundation으로 MP4 동영상의 지정 시각에서 정지 이미지를 뽑는 방법 - .cpp에 그대로 붙일 수 있는 1파일 완결판
MP4에서 「12.3초 지점의 1장」을 얻고 싶다는 요건은 꽤 평범하게 있습니다. 썸네일 생성, 검사 로그, 감시 영상의 대표 프레임, 장비 로그의 증거 등입니다.
다만 Media Foundation에서는 여기가 살짝 자연스럽지 않습니다. SetCurrentPosition 뒤에 ReadSample을 1번 부르면 끝나 보이지만, 실제로는 key frame, timestamp, stride, 이미지 상하 방향, RGB32의 4바이트째 등이 얽힙니다. 거칠게 진행하면 시각이 약간 어긋나거나, 이미지가 상하가 뒤집히거나, PNG가 묘하게 투명해지는 수수하게 싫은 사고가 일어납니다.
Media Foundation의 전체상 자체는 이전에 쓴 Media Foundation이란 무엇인가 - COM과 Windows 미디어 API의 얼굴이 보이는 이유 도 참고가 됩니다. 이번에는 거기서 한 층 내려와 MP4에서 1장 뽑는 것에만 좁힙니다.
이 글에서는 IMFSourceReader를 사용해 MP4에서 지정 시각에 가장 가까운 정지 이미지를 1장 꺼내 PNG로 저장하는 부분까지를, 실무에서 밟기 쉬운 함정 포함으로 정리합니다. 게다가 끝에는 Visual Studio C++ 콘솔 앱 프로젝트의 .cpp에 그대로 붙이기 쉬운 1파일 완결 코드 를 뒀습니다. 글 속 잘린 코드는 없고, 마지막 1블록만 가져가면 도는 구성입니다.
1. 먼저 결론
먼저 결론만 정리하면 이렇습니다.
- MP4에서 1장 뽑는다면 이번에는
Media Session보다Source Reader쪽이 입구로서 자연스럽습니다 IMFSourceReader::SetCurrentPosition은 exact seek를 보증하지 않습니다. 보통 target보다 조금 앞, 특히 key frame 쪽으로 쏠리므로, 그 뒤ReadSample을 진행해 목표 시각 전후를 비교할 필요가 있습니다ReadSample은 성공이어도pSample == nullptr일 수 있습니다.HRESULT뿐 아니라flags와pSample도 봅니다- 출력 미디어 타입을
MFVideoFormat_RGB32로 맞추면 저장하기 쉽습니다 - 다만
RGB32의 4바이트째는 alpha라고는 할 수 없으므로 그대로 PNG로 쓰면 투명 이미지가 될 수 있습니다. 저장 전에0xFF를 넣어 불투명으로 해 두는 게 안전합니다 - 행별
stride와 top-down / bottom-up을 거칠게 다루면 이미지가 무너지므로, 꺼낸 sample은 일단 top-down의 연속 BGRA 로 다듬은 뒤 PNG로 넘기는 게 편합니다
요컨대 seek -> 1번 읽음 -> 저장은 살짝 거칠고, seek -> timestamp를 보면서 전후 비교 -> stride를 의식해 복사 -> PNG 저장 정도까지 하면 꽤 안정적입니다.
2. 이 글의 전제
이번에는 다음 전제로 진행합니다.
- 입력은 로컬 MP4 파일
- 원하는 건 1장의 정지 이미지
- 「지정 시각 정확히」가 아니라 「지정 시각에 가장 가까운 프레임」을 반환
- 구현은 동기 모드의
IMFSourceReader - 저장 형식은 WIC를 쓴 PNG
- 외부 라이브러리를 쓰지 않고 Windows 표준 API만으로 완결
- 중간에 해상도가 바뀌지 않는 일반적인 MP4를 전제
재생, 음성 동기화, 시크바, UI 연동까지 할 거라면 다른 설계도 있지만, 1 프레임 얻고 싶다 는 용도라면 이쪽이 꽤 명료합니다.
3. 먼저 볼 정리표
3.1. 처리 흐름
| 할 일 | 쓰는 API | 역할 |
|---|---|---|
| MP4 열기 | MFCreateSourceReaderFromURL |
파일에서 미디어 소스 생성 |
| 동영상만 고르기 | SetStreamSelection |
음성을 읽지 않도록 |
| RGB32로 변환 | SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING |
저장하기 쉬운 uncompressed frame을 얻는다 |
| 지정 시각으로 이동 | SetCurrentPosition |
100ns 단위로 seek |
| 프레임 읽기 | ReadSample |
디코드된 sample을 1장씩 취득 |
| 직전 / 직후 비교 | sample timestamp | 지정 시각에 가장 가까운 1장을 결정 |
| PNG로 저장 | WIC | 이미지 파일로 쓰기 |
3.2. 이번 판정 룰
「지정 시각의 정지 이미지」라고 해도 동영상은 연속량이 아니라 이산 프레임입니다. 그러니 구현에서는 어떤 룰로 1장을 고를지 먼저 정하는 편이 편합니다.
이번에는 다음 룰로 합니다.
- seek 후
ReadSample을 진행 timestamp < target의 마지막 sample을 보유timestamp >= target의 최초 sample이 오면 직전 sample과 현재 sample의 차이를 비교- target에 더 가까운 쪽을 채용
이걸로 「target 이후의 최초 1장」이 아니라 target에 가장 가까운 1장 을 얻기 쉬워집니다.
3.3. 처리 이미지
처리의 머리부터 끝까지는 대략 input.mp4 -> Source Reader 생성 -> RGB32 요구 -> seek -> ReadSample 반복 -> target 전후 비교 -> top-down BGRA로 다시 채움 -> WIC로 PNG 저장 입니다.
겉보기는 단순하지만 seek 정밀도, sample의 null, stride, 4바이트째 취급 각각에 작은 함정이 있습니다. 여기를 밟지 않으면 꽤 자연스럽게 쓸 수 있습니다.
4. 먼저 잡아 둘 함정
4.1. SetCurrentPosition은 exact seek가 아니다
Microsoft Learn의 IMFSourceReader::SetCurrentPosition 에도 있듯 이는 exact seeking을 보증하지 않습니다. 동영상에서는 보통 지정 위치보다 조금 앞, 특히 key frame 쪽으로 쏠립니다. 게다가 그 뒤 ReadSample 을 진행해 목표 위치까지 전진하는 전제입니다.
그래서 다음 구현은 꽤 위태롭습니다.
SetCurrentPosition(target)ReadSample을 1번- 그 frame을 저장
GOP가 긴 동영상에서는 이걸로 꽤 평범하게 어긋납니다. 여기가 수수한 진창입니다.
4.2. ReadSample은 성공이어도 pSample == nullptr일 수 있다
ReadSample은 S_OK여도 ppSample이 NULL일 때가 있습니다. 끝이라면 MF_SOURCE_READERF_ENDOFSTREAM, 스트림 갭이라면 MF_SOURCE_READERF_STREAMTICK 등의 flag가 돌아옵니다.
그래서 HRESULT만 보고 pSample을 즉시 참조하는 건 위험합니다. HRESULT, flags, pSample을 3점 세트로 보는 정도가 안전합니다.
4.3. stride와 상하 방향을 거칠게 다루면 이미지가 무너진다
이미지 버퍼는 width * bytesPerPixel로 일직선으로 채워져 있다고는 할 수 없습니다. 행 끝에 padding이 들어가는 경우도 있고, RGB 계는 bottom-up일 수도 있습니다. Microsoft Learn의 Image Stride 와 Uncompressed Video Buffers 에서도 이 점은 상당히 분명히 쓰여 있습니다.
특히 중요한 건 다음 2점입니다.
IMF2DBuffer::Lock2D는 scan line 0의 선두 포인터 와 실제 stride 를 돌려준다- bottom-up 이미지에서는 stride가 음수가 될 수 있다
이번에는 Microsoft Learn의 helper 사고방식을 가져와 최종적으로 top-down의 연속 BGRA 버퍼 에 다시 채운 뒤 PNG로 넘깁니다. 여기를 먼저 다듬어 두면 저장 측이 꽤 단순해집니다.
4.4. MFVideoFormat_RGB32의 4바이트째를 alpha로 결정 짓지 않기
MFVideoFormat_RGB32는 이름의 공기에 반해 PNG에 그대로 넘길 수 있는 「예쁜 RGBA」가 아닙니다. Windows의 32bit RGB는 byte 0, 1, 2가 B, G, R이고 byte 3은 alpha일 수도 ignore일 수도 있습니다. ARGB32가 아닌 점이 중요합니다.
여기를 GUID_WICPixelFormat32bppBGRA라 생각하고 그대로 저장하면 4바이트째에 0이 들어가 이미지가 묘하게 투명해질 수 있습니다. 이번에는 저장 전에 alpha를 0xFF로 채워 완전 불투명으로 하는 방침으로 합니다.
5. 구현의 흐름
5.1. Source Reader를 동기 모드로 생성
이번에는 1장만 얻으면 되므로 비동기 callback이 아니라 동기 ReadSample로 합니다. 동기 모드에서는 ReadSample이 다음 sample까지 block하지만, 단발 정지 이미지 추출이라면 구현이 꽤 자연스럽습니다.
Reader 생성 시에는 다음을 합니다.
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE- 모든 stream을 일단 off
MF_SOURCE_READER_FIRST_VIDEO_STREAM만 on- 출력 type을
MFMediaType_Video/MFVideoFormat_RGB32로 설정
이걸로 뒷단은 「RGB32 프레임을 받는」 전제로 쓰기 쉬워집니다.
5.2. seek 후 timestamp를 보면서 채운다
SetCurrentPosition 후에 바로 저장하지 않습니다. ReadSample로 sample을 읽으며 target보다 앞의 마지막 1장과 target을 넘은 최초 1장을 비교합니다.
이 한 수고로 seek의 거칢을 꽤 흡수할 수 있습니다.
5.3. sample에서 top-down BGRA로 정형
꺼낸 sample은 그대로 PNG로 쓰지 말고, 일단 top-down의 BGRA 버퍼로 다시 채웁니다.
ConvertToContiguousBuffer로 1개의 buffer로 만듭니다BufferLockhelper로 scan line 0과 actual stride를 얻습니다- 행별로 top-down buffer로 복사합니다
- alpha를
0xFF로 합니다
이걸로 저장 측은 「그냥 32bpp BGRA 이미지」로 다룰 수 있습니다.
5.4. PNG 저장은 WIC에 맡긴다
저장은 WIC의 IWICBitmapEncoder / IWICBitmapFrameEncode를 씁니다. Media Foundation에서 프레임을 얻고 WIC로 이미지화한다는 분담입니다. 여기는 Windows 표준 API만으로 완결됩니다.
6. 실무에서의 체크리스트
| 항목 | 볼 것 | 놓치면 일어나기 쉬운 것 |
|---|---|---|
| seek 정밀도 | SetCurrentPosition 직후 1번으로 결정하지 말 것 |
지정 시각보다 꽤 앞의 frame을 저장 |
| sample의 NULL | HRESULT, flags, pSample을 전부 볼 것 |
끝이나 stream tick에서 null dereference |
| stride | actual stride와 상하 방향을 흡수 | 이미지가 무너지고 상하가 뒤집힘 |
| RGB32의 4 byte째 | alpha를 0xFF로 |
투명 PNG가 됨 |
| 시각 범위 | 0 <= target < duration을 지킴 |
끝 부근에서 의도하지 않은 거동 |
| 연속 추출 | Reader를 다시 만들지 말고 seek를 반복 | 쓸데없이 느림 |
| copy 횟수 | 대량 처리에서는 ConvertToContiguousBuffer 비용을 의식 |
CPU와 메모리 대역을 여분 사용 |
| 포맷 변화 | 중간에 해상도가 바뀌는 특수 동영상은 다른 설계로 | 폭·높이 전제가 깨짐 |
7. 빌드와 실행 메모
이 글 끝의 코드는 Visual Studio C++ 콘솔 앱에 1개의 .cpp로 추가하기 쉬운 형태 로 되어 있습니다.
잡아 두면 편한 점은 다음입니다.
#pragma comment(lib, ...)을 넣어 두었으므로 추가 링커 설정은 기본적으로 불필요wmain을 쓰므로 커맨드라인 인자는 Unicode 그대로 다룰 수 있음- 기본 Console App 템플릿에
pch.h나stdafx.h가 있는 경우도 붙이기 쉽도록, 코드 맨 앞에서__has_include를 사용해 줍도록 되어 있음 - 그래도 프로젝트 쪽에서 독자 프리컴파일 헤더를 강제한다면 이
.cpp만 「프리컴파일 헤더 사용 안 함」으로 하면 통과 - 실행 구성은 x64 추천
사용법은 ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png> 입니다. 예컨대 ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png 처럼 실행합니다.
8. 정리
Media Foundation으로 MP4에서 지정 시각의 정지 이미지를 꺼낼 때는 SetCurrentPosition과 ReadSample만 보면 살짝 부족합니다. 실제로는,
- seek는 exact가 아니다
- frame은 timestamp를 보고 전후 비교하는 게 좋다
ReadSample성공이어도 sample이 없을 수 있다stride와 이미지 방향을 흡수하고 저장한다RGB32의 4바이트째를 alpha로 결정 짓지 않는다
이 정도까지 잡아 두면 꽤 사고가 줄어듭니다.
이번 샘플은 1장을 제대로 뽑는 것에 좁힌 최소 구성으로서는 꽤 쓰기 편할 것입니다. 썸네일 생성, 감시 영상의 대표 frame 저장, 검사 로그의 증거 출력 근처에는 그대로 가져오기 쉬운 구성입니다.
9. 참고 자료
- 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. .cpp에 그대로 붙일 수 있는 풀 코드
아래 1블록이 그대로 Visual Studio C++ 콘솔 앱 프로젝트로 가져갈 전제의 코드입니다. 커맨드라인 인자는 input.mp4, seconds, output.png 순서입니다. 1파일 완결 구성으로 해 두어 프로젝트에 붙이기 쉬운 형태입니다.
#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);
// MFVideoFormat_RGB32의 4 byte째는 alpha라고 할 수 없으므로,
// 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;
}
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Media Foundation으로 MP4 동영상의 각 프레임에 이미지와 문자를 구워 넣는 방법 - Source Reader / 드로잉 / 색 변환 / Sink Writer 정리와 .cpp에 그대로 붙일 수 있는 1파일 완결판
MP4 동영상의 각 프레임에 로고나 타임스탬프, 작업자 이름을 구워 넣어 새 MP4를 만드는 방법을, Source Reader 디코드 -> GDI+ 합성 -> NV12 색 변환 -> Sink Writer 재인코딩의 흐름과, Visual Studi...
Media Foundation에서 YUV 프레임을 RGB로 변환하는 방법 - Source Reader의 자동 변환과 직접 변환을 원리부터 정리
Media Foundation에서 NV12·YUY2 같은 YUV 프레임을 RGB로 옮기는 두 가지 길을 정리합니다. Source Reader의 자동 RGB32 변환과 직접 변환을 색공간·서브샘플링·stride 관점에서 비교하고 BT.601/709...
Media Foundation이란 무엇인가 - COM과 Windows 미디어 API의 얼굴이 보이는 이유
Media Foundation을 만지면 왜 COM의 얼굴이 진해지는지를, CoInitializeEx와 MFStartup의 분담, IMFAttributes와 GUID, IMFActivate, Source Reader / Sink Writer / M...
공유 메모리를 사용할 때의 함정과 베스트 프랙티스 - 동기, 가시성, 수명, ABI, 보안을 먼저 정리
공유 메모리는 단순히 빠른 IPC가 아니라 동기, 가시성, 수명, ABI, 권한의 책임을 앱 측이 떠맡는 구조입니다. 본 글은 함정과 베스트 프랙티스를 정리하여 SPSC 링 버퍼나 더블 버퍼, 고정 헤더, 오프셋 참조 등 사고율을 내리는 설계 첫...
C#을 Native AOT로 네이티브 DLL로 만드는 방법 - UnmanagedCallersOnly로 C/C++에서 호출하기
.NET의 Native AOT와 UnmanagedCallersOnly로 C# 클래스 라이브러리를 네이티브 DLL로 발행해 C/C++에서 in-process로 호출하는 구성을, 핸들 기반 수명 관리와 에러 코드, C ABI 설계 요령으로 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.