用 Media Foundation 从 MP4 的指定时刻提取静止图像的方法

· 更新日期: · · Media Foundation, C++, Windows 开发, WIC

更新记录(2 条,最后更新 2026年09月03日)

本文的修改记录。已保存的更新前版本,可通过带有 DOI 的永久链接阅读。

本文此前是日文原文的节译,缺少大量章节、表格、Mermaid 图、图题、脚注与 FAQ。现已改写为日文原文的完整译文,技术主张与日文版一致,并补上了此前缺失的图与表,同时统一了全篇的术语译法。 查看更新前的版本 (DOI: 10.5281/zenodo.22276784)
补充了日文原文中已有的咨询引导(consultation_services)。正文内容没有改动。 查看更新前的版本 (DOI: 10.5281/zenodo.21615461)
首次发布
引用本文(DOI: 10.5281/zenodo.21615460)

本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。

小村 豪(2026)。《用 Media Foundation 从 MP4 的指定时刻提取静止图像的方法》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615460 https://comcomponent.com/zh-CN/blog/2026/03/15/000-media-foundation-extract-still-image-from-mp4-at-specific-time/

DOI(最新版本)
10.5281/zenodo.21615460
DOI(此版本)
10.5281/zenodo.22281977

从 MP4 中取出“第 12.3 秒的那一帧”,这类需求相当常见。比如生成缩略图、检验日志、监控视频的代表帧、设备日志的留证等等。

不过在 Media Foundation 里,这件事并没有那么顺理成章。看上去只要在 SetCurrentPosition 之后调用一次 ReadSample 就结束了,但实际上还会牵扯到 key frame、timestamp、stride、图像的上下方向,以及 RGB32 的第 4 个字节等等。处理得草率,就会碰到时刻略有偏差、图像上下颠倒、PNG 莫名其妙变透明这类不起眼却很讨厌的问题。

处理草率时会出现的问题表示只做一次 seek、读一次、保存的实现会牵扯到 key frame、timestamp、stride、上下方向和 RGB32 的第 4 个字节,从而引发时刻偏差、上下颠倒、透明 PNG 三类问题的图。seek 之后读一次就保存时刻略有偏差图像上下颠倒PNG 莫名其妙变透明

图1:看上去到此为止就完事的实现,会引出时刻偏差、上下颠倒、透明 PNG 这些不起眼的问题。

关于 Media Foundation 的整体轮廓,之前写过的 Media Foundation 入门:用 COM 视角理解 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 这个程度,就会相当稳定。

草率的流程与稳定的流程表示 seek 之后读一次就保存的流程比较草率,而 seek 之后一边看 timestamp 一边比较前后、注意 stride 进行拷贝再保存 PNG 的流程会相当稳定的图。读一次就保存太草率seek一边看 timestamp 一边比较前后注意 stride 进行拷贝保存 PNG偏差与错乱的根源

图2:不要在 seek 之后立刻保存,中间插入前后比较和注意 stride 的拷贝,结果就会稳定。

图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 15 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle

2. 本文的前提

2.1. 目标读者与预备知识

  • 面向用 C++ 调用过 Windows COM API 的读者。只要理解用 CoInitializeEx 初始化、用 Release 释放接口指针这条流程就够了
  • 返回值 HRESULT 用 SUCCEEDED / FAILED 宏判断。失败时直接读十六进制的值。以 0x8007 开头的值来自 Win32 错误,低 16 位就是 Win32 的错误码(0x80070057 与 E_INVALIDARG 是同一个值)。以 MF_E_ 开头的 Media Foundation 专用错误定义在 mferror.h 中
  • 即使是第一次接触 Media Foundation 本身也能读下去。从 COM 视角看到的整体轮廓,整理在 Media Foundation 入门:用 COM 视角理解 API 中

2.2. 开发环境

项目 本文的前提
操作系统 Windows 10 / Windows 11
IDE Visual Studio 2022(工作负载“使用 C++ 的桌面开发”)
SDK Visual Studio 附带的 Windows SDK(包含 Media Foundation 与 WIC 的头文件和库)
项目 C++ 的“控制台应用”模板
平台 x64
附加库 无。mfplat.lib 等链接指定由代码中的 #pragma comment(lib, ...) 完成

粘贴方式以及预编译头相关的细节注意事项,整理在“7. 构建与运行的备忘”中。

2.3. 输入与输出的前提

本文的前提如下。

  • 输入是本地的 MP4 文件
  • 想要的是一张静止图像
  • 返回的不是“恰好等于指定时刻”,而是“最接近指定时刻的帧”
  • 实现采用同步模式的 IMFSourceReader
  • 保存格式是用 WIC 生成的 PNG
  • 不使用外部库,只用 Windows 标准 API 完成
  • 以中途分辨率不发生变化的一般 MP4 为前提

如果要做到播放、音频同步、进度条、与 UI 联动,那还有别的设计方式;但如果用途就是取出一帧,这种做法相当好懂。

本文前提的构成表示以本地 MP4 为输入,用同步模式的 IMFSourceReader 取得最接近指定时刻的帧,再用 WIC 保存为 PNG,且不依赖外部库的前提构成图。本地的 MP4同步模式的 IMFSourceReader最接近指定时刻的帧用 WIC 保存 PNG只用 Windows 标准 API 完成

图3:从输入到保存,全部走不依赖外部库的同步 Source Reader 构成。

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 与当前 sample 相对 target 的差值
  • 采用更接近 target 的那一个

这样取到的就不是“target 之后的第一帧”,而更容易拿到最接近 target 的那一帧。

选出最接近的一帧的判定规则表示 seek 之后推进 ReadSample,保留 target 之前的最后一个 sample,当 target 之后的第一个 sample 到来时比较两者的差值并采用更接近 target 的那一个的判定规则图。seek 之后推进 ReadSample保留 target 之前的最后一个 sampletarget 之后的第一个 sample 到来比较两者与 target 的差值采用更接近的那一个

图4:先比较前后两个候选帧再决定采用哪个,取到的就不是“target 之后的第一帧”,而是最接近的一帧。

3.3. 处理的整体印象

从头到尾大致是 input.mp4 -> 创建 Source Reader -> 请求 RGB32 -> seek -> 反复调用 ReadSample -> 比较 target 前后的帧 -> 重新整理成 top-down BGRA -> 用 WIC 保存 PNG。

看上去很简单,但 seek 的精度、sample 为 null、stride、第 4 个字节的处理这几处各自都藏着小陷阱。只要不踩这 4 个坑,实现本身会很顺。

处理中潜藏的 4 个陷阱表示处理看上去简单,但存在 seek 精度、sample 为 null、stride、RGB32 第 4 个字节这 4 个小陷阱,只要不踩到实现就能顺利收束的图。看上去简单的处理seek 的精度sample 为 nullstride 与上下方向第 4 个字节的处理不踩坑就能顺利收束

图5:看似简单的流程各处都有 4 个陷阱,只要留意这几点,实现就能顺利收束。

4. 先要掌握的陷阱

先把本章出现的术语简短地整理一下。

术语 含义
key frame(关键帧) 不参照前后帧,仅凭自身就能解码的帧。在 H.264 中 IDR 图像等就属于这一类
GOP(Group of Pictures) 从某个 key frame 到下一个 key frame 之前的一组帧。GOP 越长,key frame 的间隔就越大,seek 之后落到的位置也越容易远离指定时刻
stride 图像缓冲区中一行所占的字节数。它不一定等于 宽度 × 每像素字节数,行末可能会有 padding
top-down / bottom-up 图像缓冲区的开头是上边一行还是下边一行的区别。bottom-up 用负的 stride 值来表示
MF_SOURCE_READERF_STREAMTICK ReadSample 返回的 flag 之一,表示流中存在间隙(数据中断)。这个 flag 置位的那次调用取不到帧,需要重新读取

4.1. SetCurrentPosition 不是 exact seek

正如 Microsoft Learn 的 IMFSourceReader::SetCurrentPosition 所写,它并不保证 exact seeking。对视频而言,通常会落在指定位置稍靠前、尤其偏向 key frame 的位置。而且,前提是之后还要靠 ReadSample 继续前进到目标位置。

因此,下面这种实现相当危险。

  • SetCurrentPosition(target)
  • 调用一次 ReadSample
  • 保存那一帧

在 GOP 较长的视频里,这样做会偏。如果视频的 key frame 间隔是 2 秒,那就可能保存下比指定时刻最多早将近 2 秒的帧。

seek 之后只读一次为什么会偏表示 SetCurrentPosition 会落在指定位置稍靠前、尤其偏向 key frame 的位置,因此紧接着只读一次就保存,在 GOP 较长的视频中会保存下比指定时刻早很多的帧的图。SetCurrentPosition(target)落在 key frame 一侧稍靠前的位置只调用一次 ReadSample保存下靠前的那一帧GOP 越长偏差越大

图6:seek 会偏向 key 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 作为三件套一起看,才算稳妥。

ReadSample 结果的三点确认表示 ReadSample 即使返回 S_OK 也可能取不到 sample,因此需要把 HRESULT、flags、pSample 三者一起确认,并处理末尾和流间隙 flag 的图。ReadSample 的结果看 HRESULT看 flags看 pSample可能出现末尾或间隙的 flag即使 S_OK 也可能是 NULL

图7:不要只凭成功码就去引用,要把 HRESULT、flags、pSample 作为三件套一起确认。

4.3. 对 stride 和上下方向处理草率,图像就会错乱

图像缓冲区不一定按 width * bytesPerPixel 紧密排列。行末可能会有 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。先把这一步理顺,保存端就会相当简单。

吸收 stride 与上下方向表示用 IMF2DBuffer 的 Lock2D 取得 scan line 0 的起始指针和实际的 stride,把包括 bottom-up 时 stride 为负在内的情况都重新整理成 top-down 的连续 BGRA 缓冲区,再交给 PNG 的流程图。用 Lock2D 取得scan line 0 的起始指针实际的 stridebottom-up 时可能为负重新整理成 top-down 的连续 BGRA保存端变简单

图8:用 Lock2D 吸收实际的 stride 与上下方向,整理成 top-down 的连续 BGRA 之后再交给保存。

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,使其完全不透明。

RGB32 第 4 个字节的处理表示 MFVideoFormat_RGB32 的字节 0 到 2 是 B、G、R,而第 4 个字节不一定是 alpha,直接写入 PNG 有时会变透明,因此保存前把 alpha 填成 0xFF 使其不透明的方针图。原样保存填成 0xFF 再保存MFVideoFormat_RGB32 的帧第 4 个字节不一定是 alpha有时会变成透明 PNG完全不透明的 PNG

图9:不要对第 4 个字节想当然,保存前用 0xFF 填满,把不透明固定下来。

5. 实现流程

5.1. 以同步模式创建 Source Reader

这次只要能取到一帧就够了,所以不用异步 callback,而用同步的 ReadSample。同步模式下 ReadSample 会阻塞到下一个 sample 到来,但对于单次静止图像提取来说,实现相当直接。

创建 Reader 时要做的就是下面 4 件事。

  • MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  • 先把所有 stream 都关掉
  • 只打开 MF_SOURCE_READER_FIRST_VIDEO_STREAM
  • 把输出 type 设为 MFMediaType_Video / MFVideoFormat_RGB32

这样一来,后半段就可以按“接收 RGB32 的帧”这个前提去写了。

创建 Reader 时的 4 个步骤表示启用视频处理、先关掉所有 stream 再只打开第一个视频 stream、把输出 type 设为 RGB32 这 4 个创建 Reader 步骤及其顺序的图。启用视频处理关掉所有 stream只打开第一个视频 stream把输出 type 设为 RGB32后半段可按 RGB32 前提编写

图10:把创建 Reader 的 4 个步骤依次做完,后半段就只剩下接收 RGB32 的帧了。

5.2. seek 之后一边看 timestamp 一边推进

SetCurrentPosition 之后不要马上保存。要一边用 ReadSample 读 sample,一边比较 target 之前的最后一帧与跨过 target 的第一帧。

多花这一道功夫,就能相当程度地吸收 seek 的粗糙。

seek 之后比较前后帧的交互表示应用用 SetCurrentPosition 进行 seek 之后,反复调用 ReadSample 确认 timestamp,在跨过 target 的时点比较前后两个 sample 并选出更接近者的流程图。Source Reader应用Source Reader应用loop[直到跨过 target]SetCurrentPosition(target)ReadSamplesample 与 timestamp比较前后两个 sample,采用更接近的那一个

图11:不要在 seek 之后立刻保存,先读到跨过 target 为止,再比较前后两帧。

5.3. 把 sample 整理成 top-down BGRA

取出的 sample 不直接写入 PNG,而是先重新整理成 top-down 的 BGRA 缓冲区。

  • 用 ConvertToContiguousBuffer 合并成一段 buffer
  • 用 BufferLock helper 取得 scan line 0 与实际的 stride
  • 逐行拷贝到 top-down buffer 中
  • 把 alpha 设为 0xFF

这样保存端就可以把它当作“普通的 32bpp BGRA 图像”来处理。

从 sample 到 BGRA 的整理步骤表示用 ConvertToContiguousBuffer 合并成一段缓冲区,用 BufferLock 取得 scan line 0 与实际的 stride,逐行拷贝到 top-down 缓冲区并把 alpha 设为 0xFF 的整理步骤图。ConvertToContiguousBuffer用 BufferLock 取得 scan line 0 与 stride逐行拷贝到 top-down把 alpha 设为 0xFF就是普通的 32bpp BGRA 图像

图12:走完这 4 步整理,保存端就可以只当成普通的 32bpp BGRA 图像来处理。

5.4. PNG 保存交给 WIC

保存使用 WIC 的 IWICBitmapEncoder / IWICBitmapFrameEncode。分工是:Media Foundation 取帧,WIC 做成图像。这一段完全只靠 Windows 标准 API 就能完成。

Media Foundation 与 WIC 的分工表示 Media Foundation 负责取得帧,WIC 负责做成图像并写出 PNG 的分工图。取得帧WIC 做成图像Media FoundationBGRA 帧PNG 文件

图13:取帧由 Media Foundation 负责,成图由 WIC 负责,只用标准 API 就能走完。

6. 实际工作中的检查清单

项目 要看什么 漏看之后容易发生什么
seek 精度 不要只凭 SetCurrentPosition 之后的一次读取就定下来 保存下比指定时刻早很多的 frame
sample 为 NULL HRESULT、flags、pSample 全都要看 在末尾或 stream tick 处发生 null 解引用
stride 吸收实际的 stride 与上下方向 图像错乱、上下颠倒
RGB32 的第 4 个字节 把 alpha 设为 0xFF 变成透明 PNG
时刻范围 遵守 0 <= target < duration 在末尾附近出现意料之外的行为
连续提取 不重新创建 Reader,反复 seek 白白变慢
拷贝次数 大批量处理时要留意 ConvertToContiguousBuffer 的开销 多占用 CPU 与内存带宽
格式变化 中途分辨率会变的特殊视频要另行设计 宽高的前提被打破

其中“连续提取”和“拷贝次数”这两行,说的不是取一帧本身,而是当你把它扩展成从同一个视频里抽几十帧时才会显现出来的项目。本文的示例是取一帧就结束的结构,所以要抽多帧时,请改成不重新创建 Source Reader,而是反复执行 SetCurrentPosition 与 ReadSample。包含 Source Reader 之外的选项(如 Media Session)在内的 Media Foundation 全景图,可以看 Media Foundation 入门:用 COM 视角理解 API。

从取一帧扩展到连续提取的方式表示只取一帧时保持本文结构即可,而要从同一个视频抽多帧时应改成不重新创建 Source Reader、反复执行 seek 与 ReadSample 的扩展方式图。只有一帧多帧要抽几帧保持本文的结构不重新创建 Reader,反复 seek 与读取也要留意拷贝次数的开销

图14:扩展成抽多帧时,要改成不重新创建 Reader,而是反复 seek 与读取。

7. 构建与运行的备忘

本文最后的代码,做成了便于以单个 .cpp 文件添加到 Visual Studio 的 C++ 控制台应用中的形式。环境本身的前提整理在 2.2 中。

7.1. 构建备忘

列几个先掌握会更省事的点。

  • 代码里已经加了 #pragma comment(lib, ...),基本不需要额外的链接器设置
  • 由于使用 wmain,命令行参数可以保持 Unicode 形式处理
  • 为了在默认 Console App 模板存在 pch.h 或 stdafx.h 时也便于粘贴,代码开头用 __has_include 做了兼容处理
  • 即便如此,如果项目那边仍强制使用自定义的预编译头,只要把这一个 .cpp 设为“不使用预编译头”就能通过
  • 运行配置建议用 x64

7.2. 运行方式,以及成功时会输出什么

用法是 ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>。例如可以这样执行:ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png。

成功时,wmain 的最后会向标准输出打印下面 3 行。

Saved: C:\work\frame.png
Requested: 12.345 sec
Actual: (采用的那一帧的显示时刻) sec

Requested 就是传给参数的秒数本身,Actual 是实际采用的那一帧的 timestamp。由于按 3.2 的规则在前后两帧中取更接近的一个,两者之差会落在帧间隔的大约一半以内(29.97fps 时上限大致是 17ms 左右)。如果这里相差几百毫秒以上,就要怀疑是不是 seek 之后只读了一次,也就是 4.1 讲的前后比较没有生效。

如何看 Requested 与 Actual 之差表示 Requested 与 Actual 之差落在帧间隔一半以内即为正常,若相差几百毫秒以上则应怀疑 seek 之后的前后比较没有生效的诊断流程图。在帧间隔的一半以内几百 ms 以上看 Requested 与 Actual 之差前后比较生效了怀疑前后比较没有生效确认是不是 seek 之后只读了一次等情况

图15:如果差值远超帧间隔的一半,就要怀疑前后比较的实现。

动作确认的目标是这 3 点。

  • 退出码为 0,并且 Saved: 那一行显示的是指定的输出路径
  • 打开输出的 PNG,能看到该时刻的画面,且方向正确(没有上下颠倒)
  • PNG 的宽高与原视频的分辨率一致,背景没有透出来(4.4 的 alpha 填充生效了)

失败时会向标准错误输出打印 Failed. HRESULT = 0x........。这个值按 2.1 的要领去读。当指定的秒数大于等于视频长度时,值会是 0x80070057(E_INVALIDARG)。

确认运行结果的流程表示运行成功时确认 3 行输出以及 PNG 的方向、分辨率与不透明,失败时读取标准错误输出的 HRESULT,指定秒数大于等于视频长度时为 E_INVALIDARG 的确认流程图。成功失败执行确认 3 行输出与 PNG读标准错误的 HRESULT秒数大于等于视频长度时为 E_INVALIDARG

图16:成功就确认输出与 PNG 的这 3 点,失败就从 HRESULT 的值读出原因。

8. 总结

用 Media Foundation 从 MP4 中取出指定时刻的静止图像时,只盯着 SetCurrentPosition 和 ReadSample 还差一点。实际上,

  • seek 不是 exact 的
  • frame 最好看着 timestamp 做前后比较
  • ReadSample 即使成功也可能没有 sample
  • 先吸收 stride 与图像方向再保存
  • 不要断定 RGB32 的第 4 个字节就是 alpha

把这些都掌握住,就会相当不容易出问题。

要掌握的 5 点表示 seek 不是 exact 的、按 timestamp 做前后比较、确认成功时也可能没有 sample、吸收 stride 与方向、不要断定 RGB32 第 4 个字节这 5 点掌握后就不容易出问题的图。seek 不是 exact 的按 timestamp 做前后比较确认成功时也可能没有 sample吸收 stride 与方向不要断定第 4 个字节不容易出问题的实现

图17:这 5 个要点齐了,指定时刻的静止图像提取就相当不容易出问题。

本文的示例,是聚焦在把一帧取干净这件事上的最小构成。用在生成缩略图、保存监控视频的代表 frame、输出检验日志的留证这类场景,可以直接照搬。

9. 参考资料

10. 可直接粘贴到 .cpp 的完整代码

下面这一整块,就是可以直接拿到 Visual Studio 的 C++ 控制台应用项目里的代码。命令行参数依次是 input.mp4、seconds、output.png。做成了单文件完整的结构,便于粘贴到项目中。

因为是很长的一整块,先把哪个函数对应正文的哪一节放在这里。无论是阅读还是在跑不起来时排查原因,从这张对照表入手都更快。

代码中的函数 / 类 对应的正文 作用,以及所应对的陷阱
MediaFoundationScope 5.1 把 CoInitializeEx 与 MFStartup 的初始化和收尾归拢到一起
CreateConfiguredSourceReader 5.1 创建 Reader,只选择视频流,并请求 MFVideoFormat_RGB32
GetPresentationDuration 6.(时刻范围) 取得视频长度,确认是否满足 0 <= target < duration
SeekSourceReader 4.1 / 5.2 用 SetCurrentPosition 进行 seek。光靠这里还做不到 exact
ReadNearestVideoSample 3.2 / 4.1 / 4.2 / 5.2 比较 target 前后并选出一帧。flags 与 pSample 为 null 的情况也在这里处理
GetDefaultStride 4.3 在没有 MF_MT_DEFAULT_STRIDE 时计算并补上 stride
BufferLock 4.3 / 5.3 用 IMF2DBuffer::Lock2D 取得 scan line 0 与实际的 stride
CopyContiguousBufferToTopDownBgra 4.3 / 4.4 / 5.3 逐行重新装入 top-down 缓冲区,并把第 4 个字节填成 0xFF
CopySampleToTopDownBgra 5.3 取出 frame size 与 stride,再调用上面的拷贝
SaveBgraToPng 5.4 用 WIC 把 32bpp BGRA 写出成 PNG
ExtractFrameFromMp4ToPng 5. 整体 依次调用上面这些函数的入口
TryParseSeconds / wmain 7.2 参数解析,以及 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,
            &currentTimestamp,
            &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;
}

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

技术咨询 & 设计评审

如果希望在动手实现之前先把 seek 精度、缓冲区格式、stride、图像上下方向等问题梳理清楚,可以按技术咨询与设计评审的方式,从方案梳理开始介入。

常见问题

汇总了咨询这一主题时常见的问题。

要从 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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表