Extracting a Still Image from an MP4 at a Specific Time with Media Foundation

· Updated: · · 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.

Failures that happen when you proceed carelesslyDiagram 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.Seek, read once, saveThe time is slightly offThe image comes out upside downThe 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 Reader is a more natural entry point here than the Media Session
  • IMFSourceReader::SetCurrentPosition does not guarantee an exact seek. It normally lands slightly before the target, biased toward a key frame, so you then need to advance with ReadSample and compare the frames on either side of the target time
  • ReadSample can succeed and still give you pSample == nullptr. Look at the flags and pSample, not just the HRESULT
  • Steering the output media type to MFVideoFormat_RGB32 makes saving easy
  • However, the fourth byte of RGB32 is not necessarily alpha, so writing it straight to a PNG can produce a transparent image. It is safest to set it to 0xFF before saving so the image is opaque
  • Handle the per-row stride and 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.

The careless flow and the stable flowDiagram 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.Reading once and saving is carelessseekCompare the timestamps on either sideCopy with stride in mindSave as PNGSource 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.

Extracting a still image from MP4 with Media FoundationDiagram 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.usesrequiresusesrequiresmay causeusesrequiresrequiresusesrequiresmay causepreventsusesusesmay causeVideo Frame Extraction at a TimestampIMFSourceReader (Source Reader)Media FoundationSeek via SetCurrentPositionIMFSourceReader::ReadSampleGOP (Group of Pictures)Key FrameStride (Image Row Pitch)IMF2DBuffer::Lock2DTop-Down / Bottom-Up Image OrientationMFVideoFormat_RGB32Unintentionally Transparent PNGForcing Alpha to 0xFFWindows Imaging Component (WIC)Null Sample from ReadSample

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 CoInitializeEx and releasing interface pointers with Release is enough
  • HRESULT return values are tested with the SUCCEEDED / FAILED macros. On failure, read the hexadecimal value as it is. Values that start with 0x8007 come from Win32 errors, and the low 16 bits are the Win32 error code (0x80070057 is the same value as E_INVALIDARG). Media Foundation specific errors that start with MF_E_ are defined in mferror.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 IMFSourceReader in 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.

The setup assumed in this articleDiagram 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.Local MP4Synchronous IMFSourceReaderFrame closest to the specified timeSave as PNG with WICWindows 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 ReadSample after the seek
  • Keep the last sample with timestamp < target
  • When the first sample with timestamp >= target arrives, 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.”

The rule for picking the closest frameDiagram 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.Advance ReadSample after the seekHold the last sample before the targetThe first sample at or after the target arrivesCompare both deltas against the targetAdopt 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.

The four traps hidden in the processingDiagram 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.Processing that looks simpleSeek precisionNull samplesStride and orientationHandling of the fourth byteAvoid them and it falls into place

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.

Why a single read right after the seek driftsDiagram 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.SetCurrentPosition (target)Lands slightly before, on the key frame sideOnly one ReadSampleSaves an earlier frameThe 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.

The three checks on a ReadSample resultDiagram 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.Result of ReadSampleLook at the HRESULTLook at the flagsLook at pSampleEnd-of-stream or gap flags are possibleNULL 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::Lock2D returns 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.

Absorbing stride and orientationDiagram 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.Get them with Lock2DPointer to the start of scan line 0Actual strideCan be negative for bottom-upRepack into contiguous top-down BGRAThe 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.

Handling the fourth byte of RGB32Diagram 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.Save it as it isFill with 0xFF, then saveMFVideoFormat_RGB32 frameThe fourth byte is not necessarily alphaCan become a transparent PNGFully 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.

The four steps when creating the ReaderDiagram 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.Enable video processingTurn all streams offTurn on only the first video streamSet the output type to RGB32Downstream 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.

The exchange that compares both sides after the seekDiagram 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.Source ReaderAppSource ReaderApploop[Until the target is crossed]SetCurrentPosition (target)ReadSamplesample and timestampCompare 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 BufferLock helper
  • 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.

Steps for normalizing a sample into BGRADiagram 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.ConvertToContiguousBufferscan line 0 and stride via BufferLockCopy row by row into top-downSet alpha to 0xFFJust 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.

The split between Media Foundation and WICDiagram 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.Fetches the frameWIC turns it into an imageMedia FoundationBGRA framePNG 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.

Scaling from one frame to repeated extractionDiagram 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.Just oneSeveralHow many framesKeep the setup in this articleRepeat seek and read without recreating the ReaderMind 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 unnecessary
  • wmain is used, so command-line arguments are handled in Unicode throughout
  • For default Console App templates that have pch.h or stdafx.h, the top of the code uses __has_include to pick them up, so pasting still works
  • If your project still forces its own precompiled header, set “Not Using Precompiled Headers” for just this .cpp and 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.

How to read the gap between Requested and ActualDiagram 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.Within half a frame intervalSeveral hundred ms or moreLook at the gap between Requested and ActualThe comparison is workingSuspect the comparison is not workingCheck 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).

The flow for checking the run resultDiagram 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.SuccessFailureRun itCheck the three output lines and the PNGRead the HRESULT on standard errorE_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 ReadSample may still carry no sample
  • Absorb the stride and the image orientation before saving
  • Don’t assume the fourth byte of RGB32 is alpha

Cover this much and very little is left to go wrong.

The five points to coverDiagram 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.The seek is not exactCompare timestamps on either sideCheck for a missing sample even on successAbsorb stride and orientationDo not assume the fourth byteAn 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

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,
            &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);

        // 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;
}

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

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.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog