كيفيّة استخراج صورة ثابتة من MP4 باستخدام Media Foundation - ملفّ .cpp واحد يمكن لصقه في تطبيق وحدة تحكّم C++
· 小村 豪 · Media Foundation, C++, تطوير Windows, WIC
الحاجة إلى إطار واحد من ملفّ MP4 عند لحظة من قبيل 12.3 ثانية هي مطلب اعتياديّ جدّاً.
توليد الصور المصغّرة، وسجلّات الفحص، ولقطات المراقبة، وإخراج البيّنات على جانب الجهاز، كلّها تصطدم بهذا الشكل عاجلاً أو آجلاً.
لكنّ Media Foundation هنا أقلّ مباشرةً ممّا يبدو للوهلة الأولى.
للوهلة الأولى، قد يبدو أنّ SetCurrentPosition متبوعاً باستدعاء واحد لـ ReadSample يكفي. لكنّ في الواقع تتدخّل key frames و sample timestamps و stride واتّجاه الصورة والبايت الرابع من RGB32. إذا تسرّعت في تنفيذ ذلك، يمكن للإطار أن ينحرف عن الزمن المطلوب، أو أن يظهر الإخراج مقلوباً رأسيّاً، أو أن يخرج ملفّ PNG شفّافاً بشكل غريب.
أمّا الشكل العامّ لـ Media Foundation نفسه، فالمقال السابق ما هو Media Foundation - لماذا يبدأ في الإحساس بأنّه COM وواجهات Windows الإعلاميّة في آنٍ واحد رفيق مفيد.
أمّا هذا المقال فينزل طبقةً أدنى ويركّز فقط على سحب صورة ثابتة واحدة من MP4.
الهدف هنا بسيط: استخدام IMFSourceReader لاستخراج الإطار الأقرب إلى زمن مطلوب وحفظه بصيغة PNG من تطبيق سطح مكتب C++ أصيل. وفي النهاية، بدلاً من ترك المقال شذرات متناثرة، توجد نسخة في ملفّ .cpp واحد يُقصد منها أن يسهل لصقها في مشروع وحدة تحكّم Visual Studio.
1. النسخة المختصرة
- لسحب إطار واحد من MP4، يكون
Source Readerعادةً نقطة دخول أهدأ منMedia Session - لا يضمن
IMFSourceReader::SetCurrentPositionseek دقيقاً. فهو في الغالب يهبط قبل المطلوب بقليل، وغالباً قرب key frame، لذا تحتاج إلى التقدّم بـReadSampleومقارنة الـ timestamps المتجاورة - يمكن أن ينجح
ReadSampleمع إعادةpSample == nullptr، لذا يجب التحقّق منflagsوpSampleمعاً - صيغة
MFVideoFormat_RGB32ملائمة للإخراج، لكن لا ينبغي افتراض أنّ بايتها الرابع هو بالفعل قناة alpha صالحة - إن قمت بتطبيع stride واتّجاه الصورة قبل الحفظ، يصبح جانب PNG أكثر استقراراً بكثير
ولذا فالتدفّق العمليّ ليس seek -> read once -> save بقدر ما هو seek -> مقارنة الـ timestamps المحيطة -> تطبيع stride/الاتّجاه -> حفظ بصيغة PNG.
2. الافتراضات
يفترض هذا المقال ما يلي.
- المُدخَل ملفّ MP4 محلّي
- المطلوب صورة ثابتة واحدة فقط
- ينبغي أن تكون النتيجة الإطار الأقرب إلى الزمن المطلوب، لا ضمان إطار دقيق غير واقعيّ
- يستخدم التنفيذ
IMFSourceReaderالمتزامن - صيغة الإخراج هي PNG عبر WIC
- تُستخدم فقط واجهات Windows المضمّنة
- ملفّ MP4 هو ملفّ اعتياديّ لا يتغيّر دقّته في منتصف المجرى
إن كنت تحتاج أيضاً إلى التحكّم في التشغيل، أو مزامنة الصوت، أو واجهة الخطّ الزمنيّ، أو عناصر التحكّم في النقل، فإنّ التصميم يتغيّر. ولكن لـ استخراج إطار واحد، عادةً ما يكون هذا المسار هو الأسهل تفكيراً.
3. تدفّق المعالجة
| الخطوة | الـ API | الدور |
|---|---|---|
| فتح ملفّ MP4 | MFCreateSourceReaderFromURL |
إنشاء media source من ملفّ |
| اختيار مجرى الفيديو فقط | SetStreamSelection |
تخطّي الصوت |
| التحويل إلى RGB32 | SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING |
الحصول على صيغة إطار غير مضغوطة سهلة الحفظ |
| الانتقال إلى الزمن المطلوب | SetCurrentPosition |
seek بوحدات 100 نانوثانية |
| قراءة الـ samples المفكوكة | ReadSample |
سحب sample فيديو واحد في كلّ مرّة |
| مقارنة الإطار قبل وبعد المُستهدف | sample timestamp | تحديد أيّهما أقرب فعليّاً |
| الحفظ بصيغة PNG | WIC | كتابة ملفّ الصورة النهائيّ |
قاعدة الاختيار المستخدمة هنا هي:
- إجراء seek أوّلاً
- الاحتفاظ بآخر sample يكون فيه
timestamp < target - عند وصول أوّل sample بـ
timestamp >= target، مقارنة مسافته مع السابق - اختيار الإطار الأقرب أيّاً كان
ذلك يعطي إطاراً قريباً فعلاً من الزمن المطلوب، لا مجرّد «أوّل إطار بعد الـ seek».
flowchart TD
A["input.mp4 + target time"] --> B["Create Source Reader"]
B --> C["Set output type to RGB32"]
C --> D["SetCurrentPosition(target)"]
D --> E["Loop with ReadSample"]
E --> F{"timestamp < target ?"}
F -- yes --> G["Keep as previous sample"]
G --> E
F -- no --> H["Compare previous and current distance"]
H --> I["Choose the closer frame"]
I --> J["Normalize to top-down BGRA"]
J --> K["Save as PNG with WIC"]
4. الفخاخ التي تستحقّ الحسم أوّلاً
4.1. SetCurrentPosition ليس seek دقيقاً
لا يعد IMFSourceReader::SetCurrentPosition بـ seek دقيق على مستوى الإطار.
على ملفّات MP4 الحقيقيّة، فإنّه عادةً يهبط قبل المطلوب بقليل، وغالباً قرب key frame. وذلك يجعل هذا التنفيذ خطِراً:
- استدعاء
SetCurrentPosition(target) - استدعاء
ReadSampleمرّةً واحدة - حفظ ذلك الإطار
مع GOP أطول، يمكن أن تكون النتيجة أبكر بشكل ملحوظ ممّا طُلب.
4.2. يمكن أن ينجح ReadSample مع pSample == nullptr
حتّى عندما يُعيد ReadSample قيمة S_OK، يمكن أن يظلّ ppSample يساوي NULL.
في حالات نهاية المجرى أو الفجوات في المجرى، يكمن المعنى الحقيقيّ في flags. لذا فالفحص الثابت هو دائماً الثلاثيّ المؤلّف من:
HRESULTflagspSample
4.3. الـ stride والاتّجاه مهمّان
لا يمكنك أن تفترض بأمان أنّ مخزّن الصورة هو مجرّد width * bytesPerPixel معبّأة في كتلة مستويّة بترتيب الصفوف.
يمكن أن يكون هناك حشو لكلّ صفّ، كما يمكن لمخزّنات نمط RGB أن تتصرّف كصور من الأسفل إلى الأعلى تبعاً للمسار.
العلاج العمليّ هو تطبيع كلّ شيء إلى مخزّن BGRA متجاور من الأعلى إلى الأسفل قبل الحفظ.
4.4. لا تثق ثقةً عمياء بالبايت الرابع من RGB32
صيغة MFVideoFormat_RGB32 ملائمة، لكنّها ليست تلقائيّاً «BGRA نظيف 32bpp جاهز لـ PNG».
إذا كان البايت الرابع يحتوي على أصفار وقمت بتمريره مباشرةً إلى مرمّز PNG يتوقّع alpha، يمكن أن تخرج الصورة شفّافة.
في نهج هذا المقال، يُجبَر هذا البايت صراحةً على القيمة 0xFF قبل كتابة PNG.
5. تدفّق التنفيذ
5.1. إنشاء Source Reader في الوضع المتزامن
لأنّ الهدف هو إطار واحد فقط، فإنّ ReadSample المتزامن يبقي التنفيذ أهدأ من قارئ مبنيّ على callbacks.
عند الإنشاء، يكون الإعداد كالتالي:
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE- تعطيل كلّ المجاري أوّلاً
- تمكين
MF_SOURCE_READER_FIRST_VIDEO_STREAM - تعيين نوع الإخراج إلى
MFMediaType_Video+MFVideoFormat_RGB32
ذلك يجعل المراحل اللاحقة أسهل بكثير في الكتابة.
5.2. بعد الـ seek، استمرّ في القراءة حتّى يُحاط الزمن المُستهدف
لا تحفظ مباشرةً بعد الـ seek.
اقرأ إلى الأمام حتّى يصبح لديك:
- آخر sample قبل المُستهدف
- أوّل sample عند المُستهدف أو بعده
ثمّ قارن المسافات وأبقِ الأقرب.
5.3. تحويل الـ sample إلى BGRA من الأعلى إلى الأسفل
قبل الحفظ:
- استدعاء
ConvertToContiguousBuffer - قفل media buffer
- النسخ صفّاً بصفّ إلى مخزّن وجهة من الأعلى إلى الأسفل
- إجبار بايتات alpha على القيمة
0xFF
ذلك يُبقي جانب WIC بسيطاً ومتوقّعاً.
5.4. اترك WIC يتولّى كتابة PNG
تنقسم الأدوار بنظافة:
- يسحب Media Foundation إطار الفيديو
- يكتب WIC ملفّ الصورة
عادةً ما يكون هذا أقلّ التوليفات إرباكاً لهذه الحالة الاستخدامية.
6. قائمة تحقّق عمليّة
| البند | ما يجب فحصه | ما يميل إلى الانكسار خلاف ذلك |
|---|---|---|
| دقّة الـ seek | لا تحسم على أوّل sample فور تنفيذ SetCurrentPosition |
يمكن للإطار المحفوظ أن يكون أبكر بكثير ممّا هو متوقّع |
| التعامل مع sample خالٍ | افحص HRESULT و flags و pSample معاً |
يمكن أن تنهار مسارات نهاية المجرى وفجوات المجرى |
| Stride | احترم تخطيط الصفوف الفعليّ والاتّجاه | يمكن أن تنكسر الصورة أو تنقلب رأسيّاً |
| البايت الرابع من RGB32 | أجبر alpha على 0xFF قبل كتابة PNG |
يمكن أن يصبح PNG شفّافاً |
| نطاق الزمن | حافظ على 0 <= target < duration |
يصبح سلوك نهاية الملفّ فوضويّاً |
| الاستخراج المتكرّر | أعد استخدام القارئ وأعد الـ seek مراراً | تضيع وقتاً بإعادة إنشاء كلّ شيء |
| تكلفة النسخ | إن استخرجت إطارات كثيرة، فكّر في تكلفة ConvertToContiguousBuffer |
يضيع نطاق الـ CPU والذاكرة |
| تغيّرات الصيغة | عامل تغيّرات الدقّة في منتصف المجرى كمشكلة تصميم منفصلة | يمكن أن تنكسر افتراضات العرض/الارتفاع |
7. ملاحظات حول البناء والتشغيل
الكود في نهاية المقال مقصود به أن يكون سهل الإسقاط في تطبيق وحدة تحكّم C++ في Visual Studio بصورة ملفّ .cpp واحد.
أهمّ النقاط التي تستحقّ التذكّر:
#pragma comment(lib, ...)مُدرَجة سلفاً، لذلك عادةً لا يلزم إعداد إضافيّ للرابط- يُستخدم
wmain، فتبقى وسائط سطر الأوامر بصيغة Unicode بنظافة - إن كان قالب وحدة التحكّم الافتراضيّ يتوقّع
pch.hأوstdafx.h، يحاول الكود تضمينه باستخدام__has_include - إن كان المشروع لا يزال يفرض إعداد رأس مترجم مسبقاً مخصّصاً، يمكن ببساطة ضبط هذا الملفّ
.cppعلى «Not Using Precompiled Headers» - لا يزال x64 هو الافتراضيّ العمليّ
شكل سطر الأوامر هو:
ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png
8. الخلاصة
استخراج صورة ثابتة من MP4 باستخدام Media Foundation ليس صعباً، لكنّه أيضاً ليس بالبساطة المُتصوّرة من seek -> read once -> save.
الأجزاء التي تستحقّ الحسم صراحةً هي:
- الـ seek ليس دقيقاً
- ينبغي اختيار الإطار بمقارنة الـ timestamps
- يمكن أن ينجح
ReadSampleدون إعادة sample قابل للاستخدام - ينبغي تطبيع stride والاتّجاه قبل الحفظ
- لا ينبغي الوثوق بالبايت الرابع من
RGB32بشكل أعمى كـ alpha
ما إن تُعالَج هذه النقاط، يصبح سير العمل مستقرّاً بما يكفي للصور المصغّرة ولقطات المراقبة واستخراج إطارات للبيّنات.
9. المراجع
- Microsoft Learn: Using the Source Reader to Process Media Data
- Microsoft Learn:
IMFSourceReader::SetCurrentPosition - Microsoft Learn:
IMFSourceReader::ReadSample - Microsoft Learn:
IMFSourceReader::SetCurrentMediaType - Microsoft Learn:
IMF2DBuffer - Microsoft Learn:
IMF2DBuffer::Lock2D - Microsoft Learn: Uncompressed Video Buffers
- Microsoft Learn: Image Stride
- Microsoft Learn: MF_MT_FRAME_SIZE attribute
- Microsoft Learn: MF_MT_DEFAULT_STRIDE attribute
- Microsoft Learn: Native pixel formats overview (WIC)
- Microsoft Learn: Uncompressed RGB Video Subtypes
10. كود .cpp كامل يمكن لصقه مباشرةً
الكتلة الأخيرة أدناه مقصود بها الاستخدام المباشر في مشروع تطبيق وحدة تحكّم C++ في Visual Studio. وسائط سطر الأوامر هي input.mp4 و seconds و output.png، بهذا الترتيب. يُحتفظ بالكود ملفّ .cpp واحداً مكتفياً بذاته بحيث يسهل لصقه في مشروع.
#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 guaranteed to be alpha,
// so force it 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;
}
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
كيفيّة دمج الصورة والنصّ في كلّ إطار من فيديو MP4 باستخدام Media Foundation - ترتيب Source Reader والرسم وتحويل الألوان وSink Writer مع نسخة بملفّ .cpp واحد قابل للّصق مباشرةً
نُنظِّم في هذا المقال خطوات Media Foundation لرسم شعار ونصّ على كلّ إطار MP4 بعقليّة Source Reader ثمّ Sink Writer، مع نموذج C++ بملفّ وا...
كيفيّة تحويل إطارات YUV إلى RGB باستخدام Media Foundation - أنماط التحويل التلقائيّ في Source Reader والتحويل اليدويّ
دليل عمليّ لتحويل إطارات YUV إلى RGB في Media Foundation: التحويل التلقائيّ عبر Source Reader مقابل التحويل اليدويّ لـ NV12 و YUY2 مع col...
ما هو Media Foundation - لماذا يبدأ في الإحساس بأنّه COM وواجهات Windows الإعلاميّة في آنٍ واحد
مقال يوضّح لماذا يشعر مطوّرو Media Foundation بأنّهم يكتبون كود COM، ويرتّب المصطلحات الأساسيّة ونقاط الدخول مثل Source Reader و Sink Wri...
المزالق وأفضل الممارسات عند استخدام shared memory - تنظيم مسبق للتزامن، الرؤية، العمر، ABI، والأمان
نُلخّص أبرز المزالق عند استخدام shared memory ونصمّم للتزامن، الرؤية، العمر، ABI، والاستعادة، حتّى يبني القارئ تكاملاً ثابتاً منخفض الأعطال.
كيف نُحوِّل C# إلى native DLL باستخدام Native AOT - استدعاء exports من نوع UnmanagedCallersOnly من C/C++
يوضِّح هذا المقال كيف نُصدر مكتبة C# بوصفها native DLL عبر Native AOT، ونكشف نقاط دخول UnmanagedCallersOnly تُستدعى مباشرةً من C أو C++ ب...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة