Extracting a Still Image from an MP4 at a Specific Time with Media Foundation
· Updated: · Go Komura · Media Foundation, C++, Windows Development, WIC
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614497)
- First published
Cite this article(DOI: 10.5281/zenodo.21614496)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Extracting a Still Image from an MP4 at a Specific Time with Media Foundation. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614496 https://comcomponent.com/en/blog/2026/03/15/000-media-foundation-extract-still-image-from-mp4-at-specific-time/
- DOI (latest version)
- 10.5281/zenodo.21614496
- DOI (this version)
- 10.5281/zenodo.22217144
“Grab the single frame at the 12.3-second mark of an MP4” is a perfectly common requirement: thumbnail generation, inspection logs, representative frames from surveillance footage, evidence images for equipment logs, and so on.
In Media Foundation, though, this is just slightly less straightforward than it looks. It seems like calling ReadSample once after SetCurrentPosition should be the end of it, but in practice key frames, timestamps, stride, image orientation, and the fourth byte of RGB32 all get involved. Proceed carelessly and things go quietly, annoyingly wrong: the time is slightly off, the image is upside down, or the PNG comes out oddly transparent.
flowchart TB
accTitle: Failures that happen when you proceed carelessly
accDescr: Diagram showing that an implementation that only seeks, reads once, and saves runs into key frames, timestamps, stride, orientation, and the fourth byte of RGB32, and ends up with a shifted time, an upside-down image, or a transparent PNG.
rough1["Seek, read once, save"] --> tz1["The time is slightly off"]
rough1 --> ud1["The image comes out upside down"]
rough1 --> tp1["The PNG comes out oddly transparent"]
Figure 1: An implementation that looks like it is already done quietly fails in three ways: a shifted time, an upside-down image, and a transparent PNG.
For the big picture of Media Foundation itself, the earlier post An Introduction to Media Foundation - Understanding the API Through a COM Lens is a useful companion. This time we step down one level and focus solely on pulling a single frame out of an MP4.
In this article, we use IMFSourceReader to extract the still image closest to a specified time from an MP4 and save it as a PNG, together with the pitfalls you actually hit in practice. And at the end there is a single self-contained listing you can paste straight into the .cpp of a Visual Studio C++ console app project. There are no code fragments scattered through the article: take just the one block at the end and it runs.
The code in this article is also published on GitHub as a complete sample (a one-file, self-contained C++ console app).
media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)
1. The Conclusion First
Summarizing the conclusions up front:
- For pulling one frame out of an MP4, the
Source Readeris a more natural entry point here than theMedia Session IMFSourceReader::SetCurrentPositiondoes not guarantee an exact seek. It normally lands slightly before the target, biased toward a key frame, so you then need to advance withReadSampleand compare the frames on either side of the target timeReadSamplecan succeed and still give youpSample == nullptr. Look at theflagsandpSample, not just theHRESULT- Steering the output media type to
MFVideoFormat_RGB32makes saving easy - However, the fourth byte of
RGB32is not necessarily alpha, so writing it straight to a PNG can produce a transparent image. It is safest to set it to0xFFbefore saving so the image is opaque - Handle the per-row
strideand top-down / bottom-up orientation carelessly and the image breaks, so normalize the extracted sample into a top-down, contiguous BGRA buffer before handing it to PNG
In short, seek -> read once -> save is a bit sloppy. Go as far as seek -> compare around the target while watching timestamps -> copy with stride in mind -> save as PNG, and things become quite stable.
flowchart TB
accTitle: The careless flow and the stable flow
accDescr: Diagram showing that seeking, reading once, and saving is careless, and that seeking, comparing the timestamps on either side of the target, copying with stride in mind, and then saving as PNG is far more stable.
sk1["seek"] --> cmp1["Compare the timestamps on either side"]
cmp1 --> cp1["Copy with stride in mind"]
cp1 --> sv1["Save as PNG"]
sk1 -.->|"Reading once and saving is careless"| ng1["Source of drift and broken images"]
Figure 2: Instead of saving right after the seek, insert a before-and-after comparison and a stride-aware copy, and the result is stable.
Knowledge map for this article
This article lays out how to pull a single still image closest to a specified time out of an MP4 using the IMFSourceReader in Media Foundation. A seek through SetCurrentPosition is not exact and normally lands toward a key frame, so the longer the GOP of the video, the more easily error creeps in, and ReadSample has to be called repeatedly after the seek to compare the timestamps on either side of the target. ReadSample can return a null sample even when it succeeds, so that has to be checked. The extracted MFVideoFormat_RGB32 frame is shaped into top-down BGRA by absorbing the stride and the vertical orientation with IMF2DBuffer::Lock2D, and fixing the fourth byte to 0xFF as alpha before saving the image as a PNG with WIC prevents the defect where the result comes out as an unintended transparent PNG.
flowchart LR
accTitle: Extracting a still image from MP4 with Media Foundation
accDescr: Diagram showing how a seek through SetCurrentPosition on IMFSourceReader lands toward a key frame, how GOP length relates to comparing timestamps before and after with ReadSample, how Lock2D absorbs stride and top-down or bottom-up orientation, and how fixing the fourth byte of RGB32 as alpha leads to saving a PNG with WIC.
video_frame_extraction["Video Frame Extraction at a Timestamp"]
imfsourcereader["IMFSourceReader (Source Reader)"]
media_foundation["Media Foundation"]
source_reader_seek["Seek via SetCurrentPosition"]
readsample["IMFSourceReader::ReadSample"]
group_of_pictures["GOP (Group of Pictures)"]
key_frame["Key Frame"]
video_stride["Stride (Image Row Pitch)"]
imf2dbuffer_lock2d["IMF2DBuffer::Lock2D"]
top_down_bottom_up_orientation["Top-Down / Bottom-Up Image Orientation"]
mfvideoformat_rgb32["MFVideoFormat_RGB32"]
unintended_transparent_png["Unintentionally Transparent PNG"]
alpha_channel_fixup["Forcing Alpha to 0xFF"]
windows_imaging_component["Windows Imaging Component (WIC)"]
null_sample_result["Null Sample from ReadSample"]
video_frame_extraction -->|"uses"| imfsourcereader
imfsourcereader -->|"requires"| media_foundation
imfsourcereader -->|"uses"| source_reader_seek
source_reader_seek -->|"requires"| readsample
group_of_pictures -.->|"may cause"| source_reader_seek
source_reader_seek -->|"uses"| key_frame
video_frame_extraction -->|"requires"| readsample
video_frame_extraction -->|"requires"| video_stride
video_stride -->|"uses"| imf2dbuffer_lock2d
video_stride -->|"requires"| top_down_bottom_up_orientation
mfvideoformat_rgb32 -.->|"may cause"| unintended_transparent_png
alpha_channel_fixup -->|"prevents"| unintended_transparent_png
video_frame_extraction -->|"uses"| windows_imaging_component
video_frame_extraction -->|"uses"| mfvideoformat_rgb32
readsample -->|"may cause"| null_sample_result
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (15 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. Assumptions for This Article
2.1. Intended readers and assumed knowledge
- This article assumes you have called Windows COM APIs from C++ before. Knowing the pattern of initializing with
CoInitializeExand releasing interface pointers withReleaseis enough HRESULTreturn values are tested with theSUCCEEDED/FAILEDmacros. On failure, read the hexadecimal value as it is. Values that start with0x8007come from Win32 errors, and the low 16 bits are the Win32 error code (0x80070057is the same value asE_INVALIDARG). Media Foundation specific errors that start withMF_E_are defined inmferror.h- The article is written so that it still reads if Media Foundation itself is new to you. The big picture from a COM perspective is in An Introduction to Media Foundation - Understanding the API Through a COM Lens
2.2. Development environment
| Item | Assumption for this article |
|---|---|
| OS | Windows 10 / Windows 11 |
| IDE | Visual Studio 2022 (the “Desktop development with C++” workload) |
| SDK | The Windows SDK bundled with Visual Studio (it includes the Media Foundation and WIC headers and libraries) |
| Project | The C++ “Console App” template |
| Platform | x64 |
| Extra libraries | None. Linking against mfplat.lib and the rest is handled by #pragma comment(lib, ...) inside the code |
The finer points about pasting the code and about precompiled headers are collected in “7. Build and Run Notes”.
2.3. Input and output assumptions
The assumptions here are:
- The input is a local MP4 file
- What we want is a single still image
- We return “the frame closest to the specified time,” not “exactly the specified time”
- The implementation uses
IMFSourceReaderin synchronous mode - The output format is PNG via WIC
- No external libraries: everything stays within Windows standard APIs
- An ordinary MP4 whose resolution does not change mid-stream
If you need playback, audio sync, a seek bar, or UI integration, other designs apply, but for “I want one frame” this one is remarkably easy to follow.
flowchart TB
accTitle: The setup assumed in this article
accDescr: Diagram showing the setup with no external libraries, taking a local MP4 as input, using a synchronous IMFSourceReader to get the frame closest to the specified time, and saving it as a PNG with WIC.
mp1["Local MP4"] --> rd1["Synchronous IMFSourceReader"]
rd1 --> nf1["Frame closest to the specified time"]
nf1 --> png1["Save as PNG with WIC"]
rd1 -.-> std1["Windows standard APIs only"]
Figure 3: Everything from input to save runs on a synchronous Source Reader setup with no external libraries.
3. The Tables to Look at First
3.1. The processing flow
| Step | API used | Role |
|---|---|---|
| Open the MP4 | MFCreateSourceReaderFromURL |
Create a media source from the file |
| Select video only | SetStreamSelection |
Avoid reading audio |
| Convert to RGB32 | SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING |
Get easy-to-save uncompressed frames |
| Move to the target time | SetCurrentPosition |
Seek in 100ns units |
| Read frames | ReadSample |
Fetch decoded samples one at a time |
| Compare before / after | sample timestamp | Decide the single frame closest to the target |
| Save as PNG | WIC | Write out the image file |
3.2. The selection rule for this article
“The still image at a specified time” sounds simple, but video is discrete frames rather than a continuum, so implementation goes more smoothly if you decide up front which rule picks the frame.
The rule here is:
- Advance with
ReadSampleafter the seek - Keep the last sample with
timestamp < target - When the first sample with
timestamp >= targetarrives, compare the deltas of the held sample and the current one - Adopt whichever is closer to the target
This makes it easy to get the frame closest to the target, rather than “the first frame at or after the target.”
flowchart TB
accTitle: The rule for picking the closest frame
accDescr: Diagram showing the rule of advancing ReadSample after the seek, holding the last sample before the target, and, when the first sample at or after the target arrives, comparing both deltas and adopting whichever is closer to the target.
adv1["Advance ReadSample after the seek"] --> bf1["Hold the last sample before the target"]
bf1 --> af1["The first sample at or after the target arrives"]
af1 --> df1["Compare both deltas against the target"]
df1 --> pk1["Adopt whichever is closer"]
Figure 4: Comparing the two candidate frames before adopting one yields the closest frame, not simply the first frame at or after the target.
3.3. The shape of the processing
From start to finish, the flow is roughly input.mp4 -> create the Source Reader -> request RGB32 -> seek -> repeat ReadSample -> compare around the target -> repack into top-down BGRA -> save as PNG via WIC.
It looks simple, but seek precision, null samples, stride, and the handling of the fourth byte each hide a small trap. Avoid stepping into these four, and the implementation itself falls into place easily.
flowchart TB
accTitle: The four traps hidden in the processing
accDescr: Diagram showing that although the processing looks simple, it hides four small traps - seek precision, null samples, stride, and the fourth byte of RGB32 - and that avoiding them keeps the implementation straightforward.
lk1["Processing that looks simple"] --> t1["Seek precision"]
lk1 --> t2["Null samples"]
lk1 --> t3["Stride and orientation"]
t3 -.-> t4["Handling of the fourth byte"]
t1 --> okf["Avoid them and it falls into place"]
t2 --> okf
t3 --> okf
Figure 5: Four traps sit along a flow that looks simple; mind just those and the implementation stays straightforward.
4. The Pitfalls to Know in Advance
Here are short definitions of the terms this chapter uses.
| Term | Meaning |
|---|---|
| key frame | A frame that can be decoded on its own without referring to the frames around it. In H.264, IDR pictures are one example |
| GOP (Group of Pictures) | The run of frames from one key frame up to just before the next one. The longer the GOP, the wider the gap between key frames, so the position after a seek tends to land farther from the specified time |
| stride | The number of bytes one row occupies in an image buffer. It does not necessarily equal width x bytes per pixel, because padding can appear at the end of a row |
| top-down / bottom-up | Whether the start of the image buffer is the top row or the bottom row. Bottom-up is expressed as a negative stride |
MF_SOURCE_READERF_STREAMTICK |
One of the flags ReadSample returns, indicating a gap (a break in the data) in the stream. A call with this flag set yields no frame, so read again |
4.1. SetCurrentPosition is not an exact seek
As Microsoft Learn states for IMFSourceReader::SetCurrentPosition, it does not guarantee exact seeking. For video, it normally lands slightly before the requested position, biased toward a key frame. On top of that, the expectation is that you then advance to the target position with ReadSample.
Which makes this kind of implementation quite shaky:
SetCurrentPosition(target)- One
ReadSample - Save that frame
On videos with long GOPs, this drifts. If the key frames are two seconds apart, you can end up saving a frame nearly two seconds earlier than the time you asked for.
flowchart TB
accTitle: Why a single read right after the seek drifts
accDescr: Diagram showing that SetCurrentPosition lands slightly before the requested position, biased toward a key frame, so reading once immediately afterward and saving it stores a frame well before the specified time on videos with long GOPs.
sp1["SetCurrentPosition (target)"] --> kf1["Lands slightly before, on the key frame side"]
kf1 --> one1["Only one ReadSample"]
one1 --> ng2["Saves an earlier frame"]
ng2 -.-> gp1["The longer the GOP, the bigger the drift"]
Figure 6: The seek is biased toward the key frame, so a single read can save a picture a whole GOP too early.
4.2. ReadSample can succeed with pSample == nullptr
ReadSample can return S_OK while ppSample is NULL. At the end of the stream you get the MF_SOURCE_READERF_ENDOFSTREAM flag; for a stream gap you get MF_SOURCE_READERF_STREAMTICK, and so on.
Checking only the HRESULT and immediately dereferencing pSample is dangerous. The safe habit is to look at all three: HRESULT, flags, and pSample.
flowchart TB
accTitle: The three checks on a ReadSample result
accDescr: Diagram showing that ReadSample can return S_OK with a NULL sample, so the HRESULT, the flags, and pSample all have to be checked as a set of three, and the end-of-stream and stream-gap flags handled.
rs1["Result of ReadSample"] --> h1["Look at the HRESULT"]
rs1 --> f1["Look at the flags"]
rs1 --> s1["Look at pSample"]
f1 -.-> gap1["End-of-stream or gap flags are possible"]
s1 -.-> nl1["NULL is possible even with S_OK"]
Figure 7: Do not dereference on the success code alone; check the HRESULT, the flags, and pSample as a set of three.
4.3. Handle stride and orientation carelessly and the image breaks
An image buffer is not necessarily packed in a straight line of width * bytesPerPixel. Rows may carry padding at the end, and RGB formats may be bottom-up. Microsoft Learn’s Image Stride and Uncompressed Video Buffers state this quite plainly.
Two points matter most:
IMF2DBuffer::Lock2Dreturns the pointer to the start of scan line 0 and the actual stride- For bottom-up images, the stride can be negative
This article borrows the approach of the Microsoft Learn helpers and repacks everything into a top-down, contiguous BGRA buffer before passing it to PNG. Settle this first and the save side becomes considerably simpler.
flowchart TB
accTitle: Absorbing stride and orientation
accDescr: Diagram showing the flow of getting the pointer to scan line 0 and the actual stride with IMF2DBuffer Lock2D, then repacking into a top-down contiguous BGRA buffer, including the bottom-up case where the stride is negative, before handing the data to PNG.
l2d["Get them with Lock2D"] --> sl0["Pointer to the start of scan line 0"]
l2d --> ast1["Actual stride"]
ast1 -.-> neg1["Can be negative for bottom-up"]
sl0 --> pack1["Repack into contiguous top-down BGRA"]
ast1 --> pack1
pack1 --> sim1["The save side becomes simple"]
Figure 8: Absorb the actual stride and orientation with Lock2D, normalize into contiguous top-down BGRA, and only then hand it to the save step.
4.4. Don’t assume the fourth byte of MFVideoFormat_RGB32 is alpha
Despite the vibe of the name, MFVideoFormat_RGB32 is not “clean RGBA” you can pass straight to PNG. Windows 32-bit RGB has bytes 0, 1, 2 as B, G, R, and byte 3 may be alpha or may be ignored. The key point is that it is not ARGB32.
Assume it is GUID_WICPixelFormat32bppBGRA and save it as it is, and you may find zeros in the fourth byte and an oddly transparent image. The policy here is to fill alpha with 0xFF before saving so the image is fully opaque.
flowchart TB
accTitle: Handling the fourth byte of RGB32
accDescr: Diagram showing that in MFVideoFormat_RGB32 bytes 0 to 2 are B, G, and R while the fourth byte is not necessarily alpha, so writing it straight to a PNG can make the image transparent, and the policy of filling alpha with 0xFF before saving.
rgb1["MFVideoFormat_RGB32 frame"] --> b4["The fourth byte is not necessarily alpha"]
b4 -->|"Save it as it is"| tp2["Can become a transparent PNG"]
b4 -->|"Fill with 0xFF, then save"| op1["Fully opaque PNG"]
Figure 9: Do not assume anything about the fourth byte; fill it with 0xFF before saving to pin the image opaque.
5. The Implementation Flow
5.1. Create the Source Reader in synchronous mode
Since one frame is all we need, we use the synchronous ReadSample instead of asynchronous callbacks. In synchronous mode ReadSample blocks until the next sample arrives, but for one-shot still-image extraction the implementation stays very straightforward.
There are four things to do when creating the Reader:
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE- Turn all streams off first
- Turn on only
MF_SOURCE_READER_FIRST_VIDEO_STREAM - Set the output type to
MFMediaType_Video/MFVideoFormat_RGB32
After that, the downstream code can be written on the assumption that it receives RGB32 frames.
flowchart TB
accTitle: The four steps when creating the Reader
accDescr: Diagram showing the order of the four steps when creating the Reader - enable video processing, turn all streams off, turn on only the first video stream, and set the output type to RGB32.
c1["Enable video processing"] --> c2["Turn all streams off"]
c2 --> c3["Turn on only the first video stream"]
c3 --> c4["Set the output type to RGB32"]
c4 -.-> rdy1["Downstream code can assume RGB32"]
Figure 10: Work through the four Reader-creation steps in order and the downstream code only has to receive RGB32 frames.
5.2. After the seek, close in while watching timestamps
After SetCurrentPosition, do not save immediately. Read samples with ReadSample and compare the last frame before the target with the first frame that crosses it.
That one extra step absorbs most of the seek’s coarseness.
sequenceDiagram
accTitle: The exchange that compares both sides after the seek
accDescr: Diagram showing the flow where the app seeks with SetCurrentPosition, repeats ReadSample while checking the timestamps, and once it crosses the target compares the samples on either side and adopts the closer one.
participant A as App
participant R as Source Reader
A->>R: SetCurrentPosition (target)
loop Until the target is crossed
A->>R: ReadSample
R-->>A: sample and timestamp
end
A->>A: Compare the samples on either side and adopt the closer one
Figure 11: Do not save right after the seek; read on until the target is crossed, then compare the frames on either side.
5.3. Normalize the sample into top-down BGRA
Rather than writing the extracted sample to PNG directly, repack it into a top-down BGRA buffer first.
- Combine it into a single buffer with
ConvertToContiguousBuffer - Get scan line 0 and the actual stride via the
BufferLockhelper - Copy row by row into the top-down buffer
- Set alpha to
0xFF
Now the save side can treat the data as just a 32bpp BGRA image.
flowchart TB
accTitle: Steps for normalizing a sample into BGRA
accDescr: Diagram showing the normalization steps of combining the data into a single buffer with ConvertToContiguousBuffer, getting scan line 0 and the actual stride with BufferLock, copying row by row into a top-down buffer, and setting alpha to 0xFF.
cv1["ConvertToContiguousBuffer"] --> bl1["scan line 0 and stride via BufferLock"]
bl1 --> rc1["Copy row by row into top-down"]
rc1 --> al1["Set alpha to 0xFF"]
al1 --> out1["Just a 32bpp BGRA image"]
Figure 12: After the four normalization steps, the save side can treat the data as just a 32bpp BGRA image.
5.4. Leave PNG saving to WIC
Saving uses WIC’s IWICBitmapEncoder / IWICBitmapFrameEncode. Media Foundation fetches the frame and WIC turns it into an image. The whole job stays within Windows standard APIs.
flowchart LR
accTitle: The split between Media Foundation and WIC
accDescr: Diagram showing the split where Media Foundation is responsible for fetching the frame and WIC is responsible for turning it into an image and writing the PNG.
mf1["Media Foundation"] -->|"Fetches the frame"| fr1["BGRA frame"]
fr1 -->|"WIC turns it into an image"| pg1["PNG file"]
Figure 13: Media Foundation fetches the frame and WIC turns it into an image, and the whole job stays within the standard APIs.
6. A Practical Checklist
| Item | What to check | What tends to happen if missed |
|---|---|---|
| Seek accuracy | Don’t settle on the single read right after SetCurrentPosition |
You save a frame well before the requested time |
| Null samples | Check HRESULT, flags, and pSample - all of them |
Null dereference at end of stream or on a stream tick |
| Stride | Absorb the actual stride and the vertical orientation | The image breaks or comes out upside down |
| The 4th byte of RGB32 | Set alpha to 0xFF |
A transparent PNG |
| Time range | Keep 0 <= target < duration |
Unintended behavior near the end of the stream |
| Repeated extraction | Repeat seeks instead of recreating the Reader | Needlessly slow |
| Copy count | For bulk processing, mind the cost of ConvertToContiguousBuffer |
Wasted CPU and memory bandwidth |
| Format changes | Handle videos whose resolution changes mid-stream with a separate design | Width and height assumptions break |
Two of those rows, repeated extraction and copy count, matter not for single-frame extraction itself but once you scale up to pulling dozens of frames out of the same video. The sample here is built to grab one frame and exit, so when you need several frames, switch to repeating SetCurrentPosition and ReadSample without recreating the Source Reader. For a map of Media Foundation as a whole, including the alternatives to the Source Reader such as the Media Session, see An Introduction to Media Foundation - Understanding the API Through a COM Lens.
flowchart TB
accTitle: Scaling from one frame to repeated extraction
accDescr: Diagram showing that extracting a single frame can stay with the setup in this article, while pulling several frames out of the same video should switch to repeating the seek and ReadSample without recreating the Source Reader.
q3["How many frames"] -->|"Just one"| as1["Keep the setup in this article"]
q3 -->|"Several"| rp1["Repeat seek and read without recreating the Reader"]
rp1 -.-> cost1["Mind the cost of the copy count too"]
Figure 14: When scaling up to several frames, switch to repeating the seek and the read instead of recreating the Reader.
7. Build and Run Notes
The code at the end of this article is shaped to be easy to add as a single .cpp to a Visual Studio C++ console app. The assumptions about the environment itself are collected in 2.2.
7.1. Build notes
A few points that make life easier:
#pragma comment(lib, ...)directives are included, so additional linker settings are generally unnecessarywmainis used, so command-line arguments are handled in Unicode throughout- For default Console App templates that have
pch.horstdafx.h, the top of the code uses__has_includeto pick them up, so pasting still works - If your project still forces its own precompiled header, set “Not Using Precompiled Headers” for just this
.cppand it builds - x64 is the recommended configuration
7.2. How to run it, and what a success looks like
Usage is ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>. For example, run it as ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png.
On success, the end of wmain prints these three lines to standard output.
Saved: C:\work\frame.png
Requested: 12.345 sec
Actual: (presentation time of the frame that was adopted) sec
Requested is exactly the number of seconds passed on the command line, and Actual is the timestamp of the frame actually adopted. Because the rule in 3.2 takes whichever of the two neighboring frames is closer, the gap between the two stays within roughly half a frame interval (at 29.97fps, about 17ms is the rough upper bound). If that gap is several hundred milliseconds or more, suspect that the before-and-after comparison from 4.1 is not in effect, for instance because only one read happens after the seek.
flowchart TB
accTitle: How to read the gap between Requested and Actual
accDescr: Diagram showing the diagnostic flow where a gap between Requested and Actual within half a frame interval is normal, and a gap of several hundred milliseconds or more suggests the before-and-after comparison after the seek is not in effect.
dfc["Look at the gap between Requested and Actual"] -->|"Within half a frame interval"| okd["The comparison is working"]
dfc -->|"Several hundred ms or more"| ngd["Suspect the comparison is not working"]
ngd -.-> ck2["Check for things like reading only once after the seek"]
Figure 15: If the gap runs well past half a frame interval, suspect the before-and-after comparison in the implementation.
There are three goals when verifying that it works.
- The exit code is 0 and the
Saved:line shows the output path you specified - Opening the resulting PNG shows the scene at that time in the correct orientation (not upside down)
- The width and height of the PNG match the resolution of the source video, and the background is not see-through (the alpha fill from 4.4 is working)
On failure, Failed. HRESULT = 0x........ goes to standard error. Read the value the way 2.1 describes. When the requested number of seconds is at or beyond the length of the video, you get 0x80070057 (E_INVALIDARG).
flowchart TB
accTitle: The flow for checking the run result
accDescr: Diagram showing the check flow where a successful run means verifying the three output lines and the orientation, resolution, and opacity of the PNG, while a failure means reading the HRESULT on standard error, which is E_INVALIDARG when the requested seconds are at or beyond the video length.
run1["Run it"] -->|"Success"| ok3["Check the three output lines and the PNG"]
run1 -->|"Failure"| er2["Read the HRESULT on standard error"]
er2 -.-> iv1["E_INVALIDARG if the seconds are at or beyond the video length"]
Figure 16: On success, check the three points across the output and the PNG; on failure, read the cause from the HRESULT value.
8. Summary
When extracting a still image at a specified time from an MP4 with Media Foundation, looking only at SetCurrentPosition and ReadSample is not quite enough. In reality:
- The seek is not exact
- Frames are better compared around the target by timestamp
- A successful
ReadSamplemay still carry no sample - Absorb the
strideand the image orientation before saving - Don’t assume the fourth byte of
RGB32is alpha
Cover this much and very little is left to go wrong.
flowchart TB
accTitle: The five points to cover
accDescr: Diagram showing that covering five points - the seek is not exact, comparing timestamps on either side, checking for a missing sample even on success, absorbing stride and orientation, and not assuming the fourth byte of RGB32 - makes the implementation far less error-prone.
k1["The seek is not exact"] --> k2["Compare timestamps on either side"]
k2 --> k3["Check for a missing sample even on success"]
k3 --> k4["Absorb stride and orientation"]
k4 --> k5["Do not assume the fourth byte"]
k5 --> safe2["An implementation that rarely goes wrong"]
Figure 17: With all five points covered, extracting a still image at a specified time becomes far harder to get wrong.
The sample here is a minimal setup focused on pulling one frame properly. It carries over directly to thumbnail generation, saving representative frames from surveillance footage, and emitting evidence images for inspection logs.
9. References
- Complete sample code for this article: 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. The Full Code, Ready to Paste into a .cpp
The single block below is meant to be carried straight into a Visual Studio C++ console app project. The command-line arguments are, in order, input.mp4, seconds, and output.png. It is a one-file, self-contained layout, so it pastes into a project easily.
It is one long block, so here first is a map of which function corresponds to which part of the article. Whether you are reading it through or investigating something that did not work, this table is the fastest way in.
| Function or class in the code | Matching section | Role, and the pitfall it addresses |
|---|---|---|
MediaFoundationScope |
5.1 | Bundles the setup and teardown of CoInitializeEx and MFStartup |
CreateConfiguredSourceReader |
5.1 | Creates the Reader, selects only the video stream, and requests MFVideoFormat_RGB32 |
GetPresentationDuration |
6. (time range) | Gets the video length and checks that 0 <= target < duration holds |
SeekSourceReader |
4.1 / 5.2 | Seeks with SetCurrentPosition. This alone does not make it exact |
ReadNearestVideoSample |
3.2 / 4.1 / 4.2 / 5.2 | Compares the frames on either side of the target and picks one. Also handles the flags and a null pSample |
GetDefaultStride |
4.3 | Computes a stride to fall back on when MF_MT_DEFAULT_STRIDE is absent |
BufferLock |
4.3 / 5.3 | Gets scan line 0 and the actual stride via IMF2DBuffer::Lock2D |
CopyContiguousBufferToTopDownBgra |
4.3 / 4.4 / 5.3 | Repacks row by row into a top-down buffer and fills the fourth byte with 0xFF |
CopySampleToTopDownBgra |
5.3 | Pulls out the frame size and stride and calls the copy above |
SaveBgraToPng |
5.4 | Writes 32bpp BGRA out as a PNG with WIC |
ExtractFrameFromMp4ToPng |
5. as a whole | The entry point that calls the functions above in order |
TryParseSeconds / wmain |
7.2 | Argument parsing, and printing 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);
// The 4th byte of MFVideoFormat_RGB32 is not necessarily alpha,
// so force it to opaque before saving as PNG.
for (UINT32 x = 0; x < width; ++x)
{
pDestRow[static_cast<size_t>(x) * 4 + 3] = 0xFF;
}
pDestRow += destStride;
pSrcRow += actualStride;
}
*pStride = destStride;
return S_OK;
}
HRESULT CopySampleToTopDownBgra(
IMFSample* pSample,
IMFMediaType* pCurrentType,
std::vector<BYTE>& pixels,
UINT32* pWidth,
UINT32* pHeight,
UINT32* pStride)
{
if (pSample == nullptr || pCurrentType == nullptr ||
pWidth == nullptr || pHeight == nullptr || pStride == nullptr)
{
return E_POINTER;
}
*pWidth = 0;
*pHeight = 0;
*pStride = 0;
IMFMediaBuffer* pBuffer = nullptr;
GUID subtype = GUID_NULL;
UINT32 width = 0;
UINT32 height = 0;
LONG defaultStride = 0;
HRESULT hr = pCurrentType->GetGUID(MF_MT_SUBTYPE, &subtype);
if (FAILED(hr))
{
goto done;
}
if (!IsEqualGUID(subtype, MFVideoFormat_RGB32))
{
hr = MF_E_INVALIDMEDIATYPE;
goto done;
}
hr = MFGetAttributeSize(pCurrentType, MF_MT_FRAME_SIZE, &width, &height);
if (FAILED(hr))
{
goto done;
}
if (width == 0 || height == 0)
{
hr = E_UNEXPECTED;
goto done;
}
hr = GetDefaultStride(pCurrentType, &defaultStride);
if (FAILED(hr))
{
goto done;
}
hr = pSample->ConvertToContiguousBuffer(&pBuffer);
if (FAILED(hr))
{
goto done;
}
hr = CopyContiguousBufferToTopDownBgra(
pBuffer,
defaultStride,
width,
height,
pixels,
pStride);
if (FAILED(hr))
{
goto done;
}
*pWidth = width;
*pHeight = height;
hr = S_OK;
done:
SafeRelease(&pBuffer);
return hr;
}
HRESULT SaveBgraToPng(
PCWSTR outputPath,
const BYTE* pixels,
UINT32 width,
UINT32 height,
UINT32 stride)
{
if (outputPath == nullptr || pixels == nullptr)
{
return E_POINTER;
}
if (width == 0 || height == 0 || stride < width * 4)
{
return E_INVALIDARG;
}
const size_t bufferSizeSizeT = static_cast<size_t>(stride) * height;
if (bufferSizeSizeT > static_cast<size_t>(std::numeric_limits<UINT>::max()))
{
return E_INVALIDARG;
}
const UINT bufferSize = static_cast<UINT>(bufferSizeSizeT);
IWICImagingFactory* pFactory = nullptr;
IWICStream* pStream = nullptr;
IWICBitmapEncoder* pEncoder = nullptr;
IWICBitmapFrameEncode* pFrame = nullptr;
IPropertyBag2* pProps = nullptr;
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppBGRA;
HRESULT hr = CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&pFactory));
if (FAILED(hr))
{
goto done;
}
hr = pFactory->CreateStream(&pStream);
if (FAILED(hr))
{
goto done;
}
hr = pStream->InitializeFromFilename(outputPath, GENERIC_WRITE);
if (FAILED(hr))
{
goto done;
}
hr = pFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &pEncoder);
if (FAILED(hr))
{
goto done;
}
hr = pEncoder->Initialize(pStream, WICBitmapEncoderNoCache);
if (FAILED(hr))
{
goto done;
}
hr = pEncoder->CreateNewFrame(&pFrame, &pProps);
if (FAILED(hr))
{
goto done;
}
hr = pFrame->Initialize(pProps);
if (FAILED(hr))
{
goto done;
}
hr = pFrame->SetSize(width, height);
if (FAILED(hr))
{
goto done;
}
hr = pFrame->SetPixelFormat(&pixelFormat);
if (FAILED(hr))
{
goto done;
}
if (!IsEqualGUID(pixelFormat, GUID_WICPixelFormat32bppBGRA))
{
hr = WINCODEC_ERR_UNSUPPORTEDPIXELFORMAT;
goto done;
}
hr = pFrame->WritePixels(
height,
stride,
bufferSize,
const_cast<BYTE*>(pixels));
if (FAILED(hr))
{
goto done;
}
hr = pFrame->Commit();
if (FAILED(hr))
{
goto done;
}
hr = pEncoder->Commit();
done:
SafeRelease(&pProps);
SafeRelease(&pFrame);
SafeRelease(&pEncoder);
SafeRelease(&pStream);
SafeRelease(&pFactory);
return hr;
}
HRESULT ExtractFrameFromMp4ToPng(
PCWSTR inputPath,
LONGLONG targetHns,
PCWSTR outputPath,
LONGLONG* pActualTimestampHns)
{
if (inputPath == nullptr || outputPath == nullptr)
{
return E_POINTER;
}
if (targetHns < 0)
{
return E_INVALIDARG;
}
MediaFoundationScope mf;
HRESULT hr = mf.Initialize();
if (FAILED(hr))
{
return hr;
}
IMFSourceReader* pReader = nullptr;
IMFMediaType* pCurrentType = nullptr;
IMFSample* pChosenSample = nullptr;
LONGLONG durationHns = 0;
UINT32 width = 0;
UINT32 height = 0;
UINT32 stride = 0;
std::vector<BYTE> pixels;
hr = CreateConfiguredSourceReader(inputPath, &pReader);
if (FAILED(hr))
{
goto done;
}
hr = pReader->GetCurrentMediaType(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
&pCurrentType);
if (FAILED(hr))
{
goto done;
}
hr = GetPresentationDuration(pReader, &durationHns);
if (FAILED(hr))
{
goto done;
}
if (targetHns >= durationHns)
{
hr = E_INVALIDARG;
goto done;
}
hr = SeekSourceReader(pReader, targetHns);
if (FAILED(hr))
{
goto done;
}
hr = ReadNearestVideoSample(
pReader,
targetHns,
&pChosenSample,
pActualTimestampHns);
if (FAILED(hr))
{
goto done;
}
hr = CopySampleToTopDownBgra(
pChosenSample,
pCurrentType,
pixels,
&width,
&height,
&stride);
if (FAILED(hr))
{
goto done;
}
hr = SaveBgraToPng(outputPath, pixels.data(), width, height, stride);
done:
SafeRelease(&pChosenSample);
SafeRelease(&pCurrentType);
SafeRelease(&pReader);
return hr;
}
bool TryParseSeconds(PCWSTR text, LONGLONG* phns)
{
if (text == nullptr || phns == nullptr)
{
return false;
}
wchar_t* end = nullptr;
errno = 0;
const double seconds = std::wcstod(text, &end);
if (end == text || *end != L'\0' || errno != 0)
{
return false;
}
if (!std::isfinite(seconds) || seconds < 0.0)
{
return false;
}
const long double hns =
static_cast<long double>(seconds) * 10000000.0L;
if (hns < 0.0L ||
hns > static_cast<long double>(std::numeric_limits<LONGLONG>::max()))
{
return false;
}
*phns = static_cast<LONGLONG>(std::llround(hns));
return true;
}
double HnsToSeconds(LONGLONG hns)
{
return static_cast<double>(hns) / 10000000.0;
}
void PrintUsage()
{
std::fwprintf(stderr, L"Usage:\n");
std::fwprintf(stderr, L" ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>\n");
std::fwprintf(stderr, L"\nExample:\n");
std::fwprintf(stderr, L" ExtractFrameFromMp4.exe input.mp4 12.345 output.png\n");
}
int wmain(int argc, wchar_t* argv[])
{
if (argc != 4)
{
PrintUsage();
return 1;
}
LONGLONG targetHns = 0;
if (!TryParseSeconds(argv[2], &targetHns))
{
std::fwprintf(stderr, L"Invalid seconds: %ls\n", argv[2]);
return 1;
}
LONGLONG actualHns = 0;
HRESULT hr = ExtractFrameFromMp4ToPng(
argv[1],
targetHns,
argv[3],
&actualHns);
if (FAILED(hr))
{
std::fwprintf(stderr, L"Failed. HRESULT = 0x%08lX\n", static_cast<unsigned long>(hr));
return 1;
}
std::wprintf(L"Saved: %ls\n", argv[3]);
std::wprintf(L"Requested: %.3f sec\n", HnsToSeconds(targetHns));
std::wprintf(L"Actual: %.3f sec\n", HnsToSeconds(actualHns));
return 0;
}
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
How to Burn Images and Text into MP4 Frames with Media Foundation
How to burn an image and text into every frame of an MP4 with Media Foundation and produce a new MP4, organized around the roles of the S...
How to Convert YUV to RGB with Media Foundation
Two ways to get RGB from YUV in Media Foundation: let the Source Reader output RGB32, or convert NV12/YUY2 yourself with stride and color...
An Introduction to Media Foundation - Understanding the API Through a COM Lens
We explain what Media Foundation is, together with the basic vocabulary of Windows media APIs - COM, HRESULT, IMFSourceReader, MFTs - in ...
The Win32 Thread Pool API — Concurrency Without Creating Threads, via CreateThreadpoolWork
Are you spawning CreateThread calls all over your native code? This article explains the Win32 thread pool API redesigned in Vista — the ...
Named Pipes in Practice — Windows' Standard IPC from Design to Security
A practical guide to named pipes, Windows' standard inter-process communication. This article organises, from primary sources, the choice...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
Extracting still images from video using Media Foundation, the Source Reader, and WIC is a classic Windows application development implementation topic.
Technical Consulting & Design Review
If you want to sort out seek accuracy, buffer formats, stride, and image orientation before implementing, we can start with direction-setting as technical consulting and design review.
Frequently Asked Questions
Common questions about the topic of this article.
- What should I use to extract a still image at a specified time from an MP4?
- For pulling out a single frame, IMFSourceReader is a more natural entry point than the Media Session. The flow is to open the file with MFCreateSourceReaderFromURL, select only the video stream with SetStreamSelection, request MFVideoFormat_RGB32, seek with SetCurrentPosition, fetch a frame with ReadSample, and save it as a PNG with WIC. The whole job fits within Windows standard APIs, with no external libraries.
- Can SetCurrentPosition move exactly to the time I specify?
- No. IMFSourceReader::SetCurrentPosition does not guarantee exact seeking, and for video it normally lands slightly before the requested position, biased toward a key frame. You need an implementation that advances with ReadSample after the seek, watches the timestamps, compares the last sample just before the target with the first sample at or after the target, and adopts whichever is closer. An implementation that reads once after the seek and saves that frame routinely drifts on videos with long GOPs.
- Why does the PNG I saved come out transparent?
- Because the fourth byte of MFVideoFormat_RGB32 is not necessarily alpha. In Windows 32-bit RGB, bytes 0, 1, and 2 are B, G, and R, while byte 3 may be alpha or may be ignored (it is not ARGB32). Writing that straight to a PNG can produce a transparent image, so it is safest to put 0xFF into the fourth byte before saving so the image is opaque.
- What causes the image to break up or come out upside down?
- How stride and vertical orientation are handled. An image buffer is not necessarily packed in a straight line of width x bytes per pixel: padding can appear at the end of each row, and RGB formats can be bottom-up (a negative stride). Get the pointer to the start of scan line 0 and the actual stride with IMF2DBuffer::Lock2D, repack everything into a contiguous top-down BGRA buffer, and only then hand it to the PNG encoder. The save side becomes simple and the breakage goes away.