用 Media Foundation 从 MP4 视频中提取指定时间点静止图像的方法
· 小村 豪 · Media Foundation, C++, Windows 开发, WIC
从 MP4 中取出”第 12.3 秒那一帧”这样的需求相当常见。比如生成缩略图、检验日志、监控视频的代表帧、设备日志的留证等场景都会用到。
不过,在 Media Foundation 中,这件事并没有想象中那么直接。看起来只要在 SetCurrentPosition 之后调用一次 ReadSample 就结束了,但实际上会牵涉到 key frame、timestamp、stride、图像的上下方向,以及 RGB32 的第 4 个字节等问题。处理得草率的话,就会出现时间点稍有偏差、图像上下颠倒、PNG 莫名其妙变成透明这类让人不太舒服的小问题。
关于 Media Foundation 的整体概念,之前写过的 Media Foundation 是什么 - 为什么能看到 COM 与 Windows 媒体 API 的影子 也可以作为参考。本文则从那里再往下一层,只聚焦在从 MP4 中取出一帧这一点上。
本文将使用 IMFSourceReader,把”从 MP4 中取出最接近指定时间点的一张静止图像,并保存为 PNG”这件事,连同实际工作中容易踩到的坑一起整理清楚。而且,文章最后还附有一份可以直接粘贴到 Visual Studio C++ 控制台应用程序项目 .cpp 文件中的、单文件完整代码。文中不会出现零碎拆分的代码,只需要拿走最后那一整块代码就能运行。
另外,本文中出现的代码,作为一整套示例代码(单文件完整的 C++ 控制台应用程序),已经在 GitHub 上公开。
media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)
1. 先说结论
先把结论整理一下,大致是这样。
- 如果只是要从 MP4 中取出一帧,这次
Source Reader比Media Session作为入口更直接 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 -> 读取一次 -> 保存 这样的流程稍显粗糙,如果能做到 seek -> 边看 timestamp 边比较前后 -> 注意 stride 进行拷贝 -> 保存 PNG 这种程度,就会相当稳定。
2. 本文的前提
这次的前提如下。
- 输入是本地的 MP4 文件
- 想要的是一张静止图像
- 目标不是”恰好等于指定时间”,而是”返回最接近指定时间的帧”
- 实现采用同步模式的
IMFSourceReader - 保存格式使用基于 WIC 的 PNG
- 不使用外部库,只用 Windows 标准 API 完成
- 前提是分辨率在过程中不会发生变化的一般 MP4 文件
如果要实现播放、音频同步、进度条、UI 联动等功能,还有其他设计方式,但如果只是想取出一帧,这种做法会相当清晰易懂。
3. 先看整理表
3.1. 处理流程
| 要做的事 | 使用的 API | 作用 |
|---|---|---|
| 打开 MP4 | MFCreateSourceReaderFromURL |
从文件创建媒体源 |
| 只选择视频 | SetStreamSelection |
不读取音频 |
| 转换为 RGB32 | SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING |
得到便于保存的未压缩帧 |
| 移动到指定时间 | SetCurrentPosition |
以 100ns 为单位进行 seek |
| 读取帧 | ReadSample |
逐个获取已解码的 sample |
| 比较前后帧 | sample timestamp | 确定最接近指定时间的一帧 |
| 保存为 PNG | WIC | 写出为图像文件 |
3.2. 本文采用的判定规则
虽说是”指定时间的静止图像”,但视频不是连续量,而是离散的帧,因此在实现中最好先确定按什么规则来选出一帧,会更省事。
这次采用以下规则。
- seek 之后推进
ReadSample - 保留
timestamp < target的最后一个 sample - 一旦出现
timestamp >= target的第一个 sample,就比较它与前一个 sample 相对 target 的差值 - 采用更接近 target 的那一个
这样一来,取到的就不是”target 之后的第一帧”,而更容易做到最接近 target 的一帧。
3.3. 处理流程概览
从头到尾大致是这样的流程:input.mp4 -> 创建 Source Reader -> 请求 RGB32 -> seek -> 反复调用 ReadSample -> 比较 target 前后的帧 -> 重新整理为 top-down BGRA -> 用 WIC 保存 PNG。
看起来很单纯,但在 seek 的精度、sample 的 null、stride、第 4 个字节的处理上,各自都藏着小陷阱。只要不踩到这些坑,用起来会相当顺畅。
4. 先掌握这些陷阱
4.1. SetCurrentPosition 并不是精确的 seek
正如 Microsoft Learn 的 IMFSourceReader::SetCurrentPosition 文档中所写,它并不保证 exact seeking。对于视频,通常会落在指定位置稍靠前、尤其偏向 key frame 的位置。而且,前提是之后还要推进 ReadSample 前进到目标位置。
正因为如此,下面这种实现相当危险。
SetCurrentPosition(target)- 只调用一次
ReadSample - 保存那一帧
在 GOP 较长的视频中,这样做会出现相当明显的偏差。这是一个不起眼却容易踩坑的地方。
4.2. ReadSample 即便成功,也可能出现 pSample == nullptr
即便 ReadSample 返回 S_OK,ppSample 也可能是 NULL。到达末尾时会返回 MF_SOURCE_READERF_ENDOFSTREAM,出现流间隙时会返回 MF_SOURCE_READERF_STREAMTICK 等标志。
只看 HRESULT 就直接引用 pSample 是很危险的。比较安全的做法是把 HRESULT、flags、pSample 这三者放在一起确认。
4.3. 对 stride 和上下方向处理得草率,图像会崩坏
图像缓冲区并不一定是按 宽度 × 每像素字节数 紧密排列的。行末有时会存在 padding,RGB 系有时也会是 bottom-up 的。Microsoft Learn 的 Image Stride 与 Uncompressed Video Buffers 中,对这一点也写得相当明确。
尤其重要的是以下两点。
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 中,字节 0、1、2 是 B、G、R,而字节 3 可能是 alpha,也可能是被忽略的值。重要的是,它并不是 ARGB32。
如果把它当作 GUID_WICPixelFormat32bppBGRA 直接保存,第 4 个字节里有时会是 0,导致图像变得莫名其妙地透明。本文的方针是在保存前把 alpha 填为 0xFF,使其完全不透明。
5. 实现流程
5.1. 以同步模式创建 Source Reader
这次只需要取一帧就够了,因此不使用异步回调,而是采用同步的 ReadSample。在同步模式下,ReadSample 会阻塞直到取得下一个 sample,但对于单次静止图像提取来说,实现会相当直接。
创建 Reader 时要做的事情有以下 4 项。
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE- 先把所有 stream 都关闭
- 只打开
MF_SOURCE_READER_FIRST_VIDEO_STREAM - 把输出类型设置为
MFMediaType_Video/MFVideoFormat_RGB32
这样一来,后续部分就可以在”接收到的是 RGB32 帧”这一前提下编写代码,会更容易。
5.2. seek 之后边看 timestamp 边整理
在 SetCurrentPosition 之后,不会立即保存。而是通过 ReadSample 逐个读取 sample,比较 target 之前的最后一帧与跨过 target 的第一帧。
多做这一步,就能相当程度地吸收 seek 的粗糙误差。
5.3. 把 sample 整理成 top-down BGRA
取出的 sample 不会直接写入 PNG,而是先重新整理成 top-down 的 BGRA 缓冲区。
- 用
ConvertToContiguousBuffer整合成一段连续 buffer - 用
BufferLockhelper 获取 scan line 0 与实际 stride - 逐行拷贝到 top-down buffer 中
- 把 alpha 设为
0xFF
这样一来,保存端就可以把它当作”普通的 32bpp BGRA 图像”来处理。
5.4. PNG 保存交给 WIC 处理
保存使用 WIC 的 IWICBitmapEncoder / IWICBitmapFrameEncode。分工是:Media Foundation 负责取帧,WIC 负责把它图像化。这一步完全依靠 Windows 标准 API 即可完成。
6. 实际工作中的检查清单
| 项目 | 要检查的内容 | 忽略后容易发生的问题 |
|---|---|---|
| seek 精度 | 不要只靠 SetCurrentPosition 之后的一次读取来决定 |
保存到比指定时间早得多的帧 |
| sample 的 NULL | 把 HRESULT、flags、pSample 全部确认一遍 |
在末尾或 stream tick 处发生 null 引用 |
| stride | 吸收实际 stride 与上下方向的差异 | 图像崩坏、上下颠倒 |
| RGB32 的第 4 个字节 | 把 alpha 设为 0xFF |
变成透明 PNG |
| 时间范围 | 保证 0 <= target < duration |
在末尾附近出现意料之外的行为 |
| 连续提取 | 不重新创建 Reader,反复 seek | 无谓地变慢 |
| 拷贝次数 | 大批量处理时要注意 ConvertToContiguousBuffer 的开销 |
多占用 CPU 与内存带宽 |
| 格式变化 | 对于中途分辨率会变化的特殊视频,另行设计 | 宽高的前提假设被打破 |
7. 构建与运行说明
本文最后的代码,做成了便于以单个 .cpp 文件添加到 Visual Studio C++ 控制台应用程序中的形式。
列出几个可以提前留意的要点。
- 由于已经加入了
#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 并不是精确的
- 帧最好通过 timestamp 进行前后比较
ReadSample即便成功,也可能没有 sample- 保存前要先吸收
stride与图像方向的差异 - 不要断定
RGB32的第 4 个字节就是 alpha
把这些要点掌握住,就能相当程度地降低出问题的概率。
这次的示例,作为专注于准确取出一帧的最小化实现,应该相当好用。对于生成缩略图、保存监控视频的代表帧、输出检验日志的留证等场景,都可以直接拿来使用这套结构。
9. 参考资料
- 本文的示例代码一套: 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. 可直接粘贴到 .cpp 中的完整代码
以下这一整块代码,是可以直接拿到 Visual Studio C++ 控制台应用程序项目中使用的代码。命令行参数依次是 input.mp4、seconds、output.png。由于做成了单文件完整的结构,因此便于粘贴到项目中。
#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 个字节不一定是 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 视频帧叠加图片与文字的方法
本文整理如何用 Media Foundation 向 MP4 视频的每一帧叠加图片与文字并生成新的 MP4:明确 Source Reader、绘制、色彩转换、Sink Writer 各自的职责分工,并给出可直接粘贴使用的单文件 C++ 示例。
在 Media Foundation 中将 YUV 转换为 RGB 的方法 - 从原理讲透 Source Reader 自动转换与手动转换
本文整理在 Media Foundation 中把解码后的 YUV 帧转换为 RGB 的两种方式:交给 Source Reader 自动转换为 RGB32,或者自己接收 NV12/YUY2 手动转换。文中还讲解 stride、BT.601/BT.709 矩阵、limited...
共享内存的陷阱与实务最佳实践
整理在实务中使用共享内存时的陷阱,以及涵盖同步、可见性、生命周期、ABI、权限等降低事故率的设计方法。
Media Foundation 入门:用 COM 视角理解 API
本文按照最先需要掌握的顺序,结合 COM、HRESULT、IMFSourceReader、MFT 等 Windows 媒体 API 的基础术语,整理 Media Foundation 到底是什么。
不只是 appsettings.json ── Windows 业务应用的配置管理实务(环境差异化设置・敏感信息・写入位置)
本文从实务角度整理 Windows 业务应用的配置管理,涵盖 appsettings.json 的分层、IConfiguration 与 IOptions/IOptionsSnapshot/IOptionsMonitor 的选用方式、环境差异化设置、可写入配置的存放位置、敏...
常见问题
汇总了咨询这一主题时常见的问题。
- 要从 MP4 中取出指定时间点的静止图像,应该用什么?
- 如果只需要取一帧,比起 Media Session,IMFSourceReader 作为入口更直接。流程是:用 MFCreateSourceReaderFromURL 打开文件,用 SetStreamSelection 只选择视频流,请求 MFVideoFormat_RGB32,用 SetCurrentPosition 进行 seek,用 ReadSample 获取帧,再用 WIC 保存为 PNG。全程只用 Windows 标准 API 即可完成,不需要任何外部库。
- 用 SetCurrentPosition 能精确移动到指定时间点吗?
- 不能。IMFSourceReader::SetCurrentPosition 并不保证 exact seeking,对于视频,通常会落在指定位置稍靠前的地方,尤其会偏向 key frame 一侧。需要在 seek 之后继续推进 ReadSample,边观察 timestamp,边比较 target 之前的最后一个 sample 与 target 之后的第一个 sample,采用更接近目标的那一个。如果只在 seek 后读取一次就直接保存,在 GOP 较长的视频中会出现明显偏差。
- 为什么保存出来的 PNG 会变成透明的?
- 因为 MFVideoFormat_RGB32 的第 4 个字节不一定就是 alpha。Windows 的 32bit RGB 中,字节 0、1、2 是 B、G、R,而字节 3 可能是 alpha,也可能是被忽略的值(它并不是 ARGB32)。如果直接写入 PNG,有时会得到透明图像,因此比较安全的做法是在保存前把第 4 个字节设为 0xFF,使其变为不透明。
- 图像崩坏或者上下颠倒的原因是什么?
- 原因在于 stride 与上下方向的处理。图像缓冲区并不一定是按 宽度 × 每像素字节数 紧密排列的,行末可能存在 padding,而且 RGB 系有时会是 bottom-up(即 stride 为负值)。通过 IMF2DBuffer::Lock2D 获取 scan line 0 的起始指针和实际 stride,先重新整理为 top-down 的连续 BGRA 缓冲区,再交给 PNG,保存端就会变得简单,也能避免图像崩坏。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。