كيفيّة دمج الصورة والنصّ في كلّ إطار من فيديو MP4 باستخدام Media Foundation - ترتيب Source Reader والرسم وتحويل الألوان وSink Writer مع نسخة بملفّ .cpp واحد قابل للّصق مباشرةً
· 小村 豪 · Media Foundation, C++, تطوير Windows, GDI+, Direct2D, DirectWrite, H.264
العلامة المائيّة للشعار، نتائج الفحص، رقم الجهاز، اسم المُشغّل، الـ timestamp.
متطلّب إنتاج MP4 جديد بعد دمج هذه المعلومات في جميع إطارات فيديو MP4 متطلّب طبيعيّ جدّاً في واجهات المراقبة والفحص والتتبّع والتحليل.
لكن بمجرّد أن تبدأ بلمس Media Foundation، تتراصّ أمامك واجهات IMFSourceReader وIMFSample وIMFMediaBuffer وIMFTransform وIMFSinkWriter، فيصبح فجأةً من الصعب رؤية أين بالضبط تضع النصّ أو ملفّ PNG.
في هذا المقال، نرتّب أوّلاً الصورة الكاملة المتمثّلة في Source Reader -> الرسم -> تحويل الألوان -> Sink Writer، ثمّ نعرض بعد ذلك عيّنةً بملفّ واحد كامل يمكن لصقها مباشرةً في تطبيق وحدة تحكّم C++ بـ Visual Studio.
العيّنة تقرأ ملفّ MP4 محدّداً، وترسم على كلّ إطار صورةً محدّدة وعبارة HelloWorld، وتنتج ملفّ MP4 ناتجاً.
ولأنّ الأولويّة هي إمكانيّة لصق الكود وتشغيله مباشرةً، فإنّ هذه العيّنة تعيد ترميز الفيديو فقط.
يمكن إدراج remux الصوت في نفس الملفّ، لكنّ موضوع المقال هو «دمج الصورة والنصّ في كلّ إطار»، لذلك نركّز أوّلاً على ذلك.
1. الخلاصة أوّلاً
- الشكل الأساسيّ لإدخال صورة أو نصّ في كلّ إطار من MP4 هو
فكّ التشفير عبر Source Reader -> دمج على الإطار غير المضغوط -> تحويل الألوان عند الحاجة -> إعادة الترميز عبر Sink Writer. - عمليّة وضع الصورة أو النصّ بحدّ ذاتها ليست من مهامّ Media Foundation. هذا الجزء من الأنسب التفكير فيه عبر واجهات الرسم مثل
GDI+وDirect2DوDirectWriteوWIC. - إن كنت ستعود إلى
MP4(H.264)، فإنّ مرحلة تحويل تربط بينRGB32 / ARGB32السهل للرسم وNV12 / I420 / YUY2الذي يتقبّله الـ encoder بسهولة كثيراً ما تكون ضروريّة. - لتشغيل أوّل نسخة عمليّة، فإنّ بنية
Source Reader -> RGB32 -> الرسم بـ GDI+ -> NV12 -> Sink Writerبنية واضحة. - إن كانت الأولويّة للسرعة وقابليّة التوسّع، فإنّ التحوّل نحو
D3D11 / DXGI surface -> Direct2D / DirectWrite -> Video Processor MFT -> Sink Writerيفتح آفاقاً للنموّ.
2. لماذا هذه المسألة معقّدة قليلاً
«إدخال نصّ في الفيديو» هو في الواقع مزيج من أربع قصص مختلفة.
-
قصّة الحاوية والـ codec
mp4هو حاوية وليس الإطار نفسه. محتواه في الغالب بيانات مضغوطة بـH.264أوH.265. -
قصّة فكّ التشفير / الترميز
لا يمكن وضع نصّ أو PNG مباشرةً عبر واجهات الرسم ثنائيّة الأبعاد العاديّة على البيانات المضغوطة. يجب أوّلاً العودة إلى إطار غير مضغوط. -
قصّة الرسم
النصوص والشعارات ودمج PNG الشفّاف ورسم النصّ بـ anti-aliasing ليست من اختصاص Media Foundation نفسها. هذا الجزء وظيفةGDI+أوDirect2D / DirectWrite / WIC. -
قصّة فضاء الألوان وصيغة البكسل
الصيغة السهلة للرسم لا تتطابق مع الصيغة التي يحبّها الـ encoder. هذا الموضع الذي كثيراً ما يتعطّل بهدوء.
في جملة واحدة مختصرة، فإنّ التفكير الأسهل ليس «إدخال نصّ بـ Media Foundation» بل «تحريك الإطارات بـ Media Foundation، ووضع المحتوى عبر واجهات الرسم، ثمّ إجراء تحويل الألوان اللازم قبل الترميز».
3. جدول التنظيم الأوّل
| النهج | البنية | الحالات الملائمة | نقاط الانتباه |
|---|---|---|---|
| تشغيله بشكل صحيح أوّلاً | Source Reader -> RGB32 -> الدمج -> NV12 -> Sink Writer |
المعالجة الدفعيّة، الأدوات الداخليّة، التنفيذ الأوّليّ | تتزايد عمليّات النسخ والتحويل على CPU |
| رفع السرعة | D3D11 / DXGI surface -> Direct2D / DirectWrite -> Video Processor MFT -> Sink Writer |
الفيديوهات الطويلة، الدقّة العالية، المعالجة بكميّات كبيرة | يزداد عبء إدارة D3D11 وDXGI |
| تحويله إلى مكوّن قابل لإعادة الاستخدام | تنفيذه كـ custom MFT وإدراجه في الـ topology |
المؤثّرات المستخدمة في عدّة تطبيقات، أو الرغبة في دمجه في خطّ أنابيب MF | يرتفع مستوى صعوبة التنفيذ والتسجيل وتصحيح الأخطاء |
عيّنة هذا المقال تنحصر في البنية الأولى «تشغيله بشكل صحيح أوّلاً».
3.1 صورة سير المعالجة
flowchart LR
A[input.mp4] --> B[IMFSourceReader]
B --> C[uncompressed frame<br/>RGB32]
C --> D[Draw image + HelloWorld with GDI+]
D --> E[BGRA -> NV12 conversion]
E --> F[IMFSinkWriter]
F --> G[output.mp4]
B --> H[audio sample]
H --> I[copy as is<br/>or re-encode]
I --> F
النقطة المهمّة هنا أنّ الرسم نفسه ليس من مهامّ Media Foundation.
Media Foundation مسؤولة عن إدخال الإطارات وإخراجها، أمّا وضع الصورة والنصّ فيُترك لواجهات الرسم.
4. كيف نقسم خطّ الأنابيب من الناحية الذهنيّة
4.1 الإدخال يُستقبل عبر IMFSourceReader
إن كان الإدخال مسار ملفّ، فإنّ MFCreateSourceReaderFromURL بنية واضحة، وإن كان بيانات فيديو في الذاكرة، فإنّ إنشاء IMFByteStream واستخدام MFCreateSourceReaderFromByteStream بنية واضحة.
أوّل ما يجب اتّخاذ القرار بشأنه هنا هو هل نستقبل الإطار بصيغة سهلة للرسم، أم بصيغة مناسبة للـ encoder.
- لتسهيل التنفيذ:
RGB32أوARGB32 - لإيلاء الأولويّة لكفاءة الترميز: YUV مثل
NV12
لكن بما أنّ دمج النصّ وPNG أسهل بكثير في عائلة RGB، فإنّ استقبال RGB32 / ARGB32 كخطوة أولى نهج عمليّ جدّاً.
عند تفعيل MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING، يقوم Source Reader بتحويل YUV -> RGB32 وإلغاء التشابك (deinterlace) نيابةً عنك.
هذا مفيد في مرحلة «أريد أوّلاً استخراج الإطار والتعامل معه»، لكنّه يثقل في الفيديوهات الطويلة أو ذات الدقّة العالية، لذلك إن كانت السرعة مطلوبة في الإنتاج، فمن المفيد إعادة النظر في البنية لاحقاً.
4.2 دمج الصورة والنصّ يُفكَّر فيه عبر GDI+ أو Direct2D / DirectWrite
نستخرج الـ buffer من IMFSample الذي تلقّيناه من Media Foundation، ونضع فوقه الشعار أو النصّ.
عيّنة هذه المرّة تستخدم GDI+ للرسم، إعطاءً للأولويّة لـ سهولة اللصق ضمن ملفّ واحد كامل.
- يمكنه قراءة الصور
- يمكنه رسم النصّ
- التحضيرات الإضافيّة قليلة نسبيّاً
- يسهل احتواؤه في
.cppواحد لتطبيق وحدة تحكّم
من جهة أخرى، في معالجة كميّات كبيرة من الفيديو الطويل أو 4K، فإنّ D3D11 + Direct2D + DirectWrite يفتح آفاقاً أوسع للنموّ.
التسلسل الطبيعيّ هو: GDI+ للتنفيذ الأوّل، ثمّ الانتقال إلى Direct2D / DirectWrite في مرحلة عصر السرعة.
4.3 ليس من المضمون أن يقبل H.264 صيغة RGB32 كما هي
هنا أكثر النقاط التي تتعطّل عندها.
عند العودة إلى MP4(H.264)، فإنّ encoder الـ H.264 من Microsoft غالباً ما يفترض مدخلات من عائلة YUV مثل I420 / IYUV / NV12 / YUY2 / YV12.
بمعنى آخر، بعد الدمج في RGB32 / ARGB32 السهل للرسم، لا يكفي تمريره كما هو إلى IMFSinkWriter لإنهاء العمل.
لذلك يحتاج التنفيذ إلى أحد الخيارين:
- إدراج
Video Processor MFTلتحويلRGB32 / ARGB32 -> NV12 - إدخال تحويل
RGB -> NV12يدويّاً
عيّنة هذه المرّة تتبنّى الخيار الثاني وتُدرج التحويل اليدويّ، إعطاءً للأولويّة لـ اكتمال الملفّ الواحد.
في الإنتاج، فإنّ بنية إدراج Video Processor MFT التي يمكنها التعامل دفعةً واحدةً مع تحويل فضاء الألوان وتغيير الحجم وإلغاء التشابك خيار قويّ جدّاً.
4.4 الإخراج يُكتب عبر IMFSinkWriter
IMFSinkWriter يتعامل بسهولة مع إخراج الفيديو.
الفكرة بسيطة:
- نوع stream الإخراج … الصيغة التي نريد كتابتها في الملفّ
مثال:MFVideoFormat_H264 - نوع stream الإدخال … الصيغة التي يمرّرها التطبيق إلى
Sink Writer
مثال:MFVideoFormat_NV12
نُعدّ هذين بشكل منفصل.
أي من منظور Sink Writer،
- جهة التطبيق تمرّر إطار
NV12غير مضغوط - يقوم
Sink Writerبترميزه إلى H.264 وكتابته في MP4
تلك هي العلاقة.
4.5 الأسهل هو التفكير في الصوت بشكل منفصل في البداية
كثيراً ما تكون الحاجة فقط إدخال شعار أو نصّ في الفيديو دون تغيير الصوت نفسه.
في الممارسة العمليّة، فإنّ بنية:
- stream الفيديو فقط:
Source Reader -> الدمج -> Sink Writer - stream الصوت: remux مضغوط كما هو
بنية مريحة جدّاً.
لكنّ عيّنة هذه المرّة، تركيزاً على دمج الصورة والنصّ في الإطار، تجعل الإخراج MP4 يحتوي على الفيديو فقط.
نسخة الإبقاء على الصوت يسهل تتبّعها كاملةً إن أُضيفت في مرحلة التوسيع لاحقاً.
5. شروط هذه العيّنة وطريقة استخدامها
كُتب هذا الكود وفق الشروط التالية.
- Windows 10 / 11
- تطبيق وحدة تحكّم C++ في Visual Studio 2022
- بناء
x64 - ملفّ
.cppهذا لا يستخدم رؤوس مُسبقة الترجمة (precompiled headers) - عرض وارتفاع فيديو الإدخال زوجيّان
- الإدخال ملفّ فيديو MP4 عاديّ
- الإخراج MP4 يحتوي على الفيديو فقط
- الصورة بصيغة يقرأها GDI+ مثل PNG / JPEG / BMP / GIF
بما أنّ NV12 هو 4:2:0، فإنّ العرض والارتفاع يجب أن يكونا زوجيّين.
لذلك، تتعامل هذه العيّنة بوضوح مع عدم استيفاء الشرط على أنّه خطأ.
5.1 طريقة الاستخدام
- أنشئ Console App في Visual Studio
- الصق هذا الـ
.cppبالكامل - اضبط الـ
.cppعلى «عدم استخدام» الرؤوس مُسبقة الترجمة - ابنِ على
x64 - شغّل كالتالي
OverlayMp4.exe input.mp4 overlay.png output.mp4
input.mp4
الفيديو الأصليّoverlay.png
الصورة المراد دمجهاoutput.mp4
وجهة الإخراج
النصّ ثابت في kOverlayText في بداية الكود ويحتوي HelloWorld.
يمكن تعديل الموضع والحجم أيضاً عبر تعديل الثوابت في الكود.
6. الكود الكامل بملفّ واحد قابل للّصق مباشرةً في .cpp
#define NOMINMAX
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <gdiplus.h>
#include <wrl/client.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cwchar>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "mf.lib")
#pragma comment(lib, "gdiplus.lib")
using Microsoft::WRL::ComPtr;
namespace
{
const wchar_t* kOverlayText = L"HelloWorld";
const float kMarginRatio = 0.03f;
const float kImageMaxWidthRatio = 0.20f;
const float kImageMaxHeightRatio = 0.20f;
const float kMinFontPx = 24.0f;
std::string HrToHex(HRESULT hr)
{
char buf[32]{};
std::snprintf(buf, sizeof(buf), "0x%08X", static_cast<unsigned int>(hr));
return std::string(buf);
}
void ThrowIfFailed(HRESULT hr, const char* message)
{
if (FAILED(hr))
{
throw std::runtime_error(std::string(message) + " failed. HRESULT=" + HrToHex(hr));
}
}
void ThrowIfGdiplusError(Gdiplus::Status status, const char* message)
{
if (status != Gdiplus::Ok)
{
char buf[128]{};
std::snprintf(buf, sizeof(buf), "%s failed. GDI+ status=%d", message, static_cast<int>(status));
throw std::runtime_error(buf);
}
}
BYTE ClampToByte(int value)
{
if (value < 0) return 0;
if (value > 255) return 255;
return static_cast<BYTE>(value);
}
class ScopedGdiplus
{
public:
ScopedGdiplus()
{
Gdiplus::GdiplusStartupInput input;
ThrowIfGdiplusError(Gdiplus::GdiplusStartup(&token_, &input, nullptr), "GdiplusStartup");
}
~ScopedGdiplus()
{
if (token_ != 0)
{
Gdiplus::GdiplusShutdown(token_);
}
}
private:
ULONG_PTR token_ = 0;
};
class ScopedMf
{
public:
ScopedMf()
{
ThrowIfFailed(CoInitializeEx(nullptr, COINIT_MULTITHREADED), "CoInitializeEx");
comInitialized_ = true;
ThrowIfFailed(MFStartup(MF_VERSION), "MFStartup");
mfStarted_ = true;
}
~ScopedMf()
{
if (mfStarted_)
{
MFShutdown();
}
if (comInitialized_)
{
CoUninitialize();
}
}
private:
bool comInitialized_ = false;
bool mfStarted_ = false;
};
class BufferLock
{
public:
explicit BufferLock(IMFMediaBuffer* buffer)
: buffer_(buffer)
{
if (!buffer_)
{
throw std::runtime_error("BufferLock received a null buffer.");
}
buffer_.As(&buffer2D_);
}
HRESULT LockBuffer(LONG defaultStride, DWORD heightInPixels, BYTE** scanline0, LONG* actualStride)
{
if (scanline0 == nullptr || actualStride == nullptr)
{
return E_POINTER;
}
HRESULT hr = S_OK;
if (buffer2D_)
{
hr = buffer2D_->Lock2D(scanline0, actualStride);
}
else
{
BYTE* data = nullptr;
hr = buffer_->Lock(&data, nullptr, nullptr);
if (SUCCEEDED(hr))
{
*actualStride = defaultStride;
if (defaultStride < 0)
{
*scanline0 = data + (static_cast<LONG>(heightInPixels) - 1) * std::abs(defaultStride);
}
else
{
*scanline0 = data;
}
}
}
locked_ = SUCCEEDED(hr);
return hr;
}
~BufferLock()
{
if (!locked_)
{
return;
}
if (buffer2D_)
{
buffer2D_->Unlock2D();
}
else
{
buffer_->Unlock();
}
}
private:
ComPtr<IMFMediaBuffer> buffer_;
ComPtr<IMF2DBuffer> buffer2D_;
bool locked_ = false;
};
struct VideoFormatInfo
{
UINT32 width = 0;
UINT32 height = 0;
UINT32 fpsNum = 0;
UINT32 fpsDen = 0;
UINT32 parNum = 1;
UINT32 parDen = 1;
LONG sourceStride = 0;
LONGLONG defaultFrameDuration = 0;
UINT32 bitrate = 0;
};
LONG GetDefaultStride(IMFMediaType* type)
{
LONG stride = 0;
HRESULT hr = type->GetUINT32(MF_MT_DEFAULT_STRIDE, reinterpret_cast<UINT32*>(&stride));
if (SUCCEEDED(hr))
{
return stride;
}
GUID subtype = GUID_NULL;
UINT32 width = 0;
UINT32 height = 0;
ThrowIfFailed(type->GetGUID(MF_MT_SUBTYPE, &subtype), "GetGUID(MF_MT_SUBTYPE)");
ThrowIfFailed(MFGetAttributeSize(type, MF_MT_FRAME_SIZE, &width, &height), "MFGetAttributeSize(MF_MT_FRAME_SIZE)");
ThrowIfFailed(MFGetStrideForBitmapInfoHeader(subtype.Data1, width, &stride), "MFGetStrideForBitmapInfoHeader");
ThrowIfFailed(type->SetUINT32(MF_MT_DEFAULT_STRIDE, static_cast<UINT32>(stride)), "SetUINT32(MF_MT_DEFAULT_STRIDE)");
return stride;
}
UINT32 ChooseBitrate(IMFMediaType* nativeType, UINT32 width, UINT32 height, UINT32 fpsNum, UINT32 fpsDen)
{
UINT32 srcBitrate = 0;
if (SUCCEEDED(nativeType->GetUINT32(MF_MT_AVG_BITRATE, &srcBitrate)) && srcBitrate > 0)
{
return srcBitrate;
}
const double fps = static_cast<double>(fpsNum) / static_cast<double>(fpsDen);
double estimated = static_cast<double>(width) * static_cast<double>(height) * fps * 0.07;
if (estimated < 1500000.0)
{
estimated = 1500000.0;
}
if (estimated > 25000000.0)
{
estimated = 25000000.0;
}
return static_cast<UINT32>(estimated);
}
VideoFormatInfo ConfigureSourceReader(IMFSourceReader* reader)
{
ThrowIfFailed(reader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE), "SetStreamSelection(all,false)");
ThrowIfFailed(reader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE), "SetStreamSelection(video,true)");
ComPtr<IMFMediaType> nativeType;
ThrowIfFailed(reader->GetNativeMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &nativeType), "GetNativeMediaType(video)");
ComPtr<IMFMediaType> requestedType;
ThrowIfFailed(MFCreateMediaType(&requestedType), "MFCreateMediaType(video requested)");
ThrowIfFailed(requestedType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video), "SetGUID(video requested major)");
ThrowIfFailed(requestedType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32), "SetGUID(video requested subtype RGB32)");
ThrowIfFailed(reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, requestedType.Get()), "SetCurrentMediaType(video RGB32)");
ComPtr<IMFMediaType> currentType;
ThrowIfFailed(reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, ¤tType), "GetCurrentMediaType(video)");
VideoFormatInfo info;
ThrowIfFailed(MFGetAttributeSize(currentType.Get(), MF_MT_FRAME_SIZE, &info.width, &info.height), "Get video frame size");
HRESULT hr = MFGetAttributeRatio(currentType.Get(), MF_MT_FRAME_RATE, &info.fpsNum, &info.fpsDen);
if (FAILED(hr))
{
ThrowIfFailed(MFGetAttributeRatio(nativeType.Get(), MF_MT_FRAME_RATE, &info.fpsNum, &info.fpsDen), "Get video frame rate");
}
if (info.fpsNum == 0 || info.fpsDen == 0)
{
throw std::runtime_error("Video frame rate is zero.");
}
hr = MFGetAttributeRatio(currentType.Get(), MF_MT_PIXEL_ASPECT_RATIO, &info.parNum, &info.parDen);
if (FAILED(hr) || info.parNum == 0 || info.parDen == 0)
{
info.parNum = 1;
info.parDen = 1;
}
info.sourceStride = GetDefaultStride(currentType.Get());
info.defaultFrameDuration = (10000000LL * info.fpsDen) / info.fpsNum;
if (info.defaultFrameDuration <= 0)
{
throw std::runtime_error("Calculated frame duration is invalid.");
}
info.bitrate = ChooseBitrate(nativeType.Get(), info.width, info.height, info.fpsNum, info.fpsDen);
return info;
}
ComPtr<IMFSinkWriter> CreateSinkWriter(const std::wstring& outputPath, const VideoFormatInfo& videoInfo, DWORD* streamIndex)
{
if (streamIndex == nullptr)
{
throw std::runtime_error("streamIndex is null.");
}
ComPtr<IMFAttributes> attributes;
ThrowIfFailed(MFCreateAttributes(&attributes, 1), "MFCreateAttributes(sink)");
ThrowIfFailed(attributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE), "SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS)");
ComPtr<IMFSinkWriter> writer;
ThrowIfFailed(MFCreateSinkWriterFromURL(outputPath.c_str(), nullptr, attributes.Get(), &writer), "MFCreateSinkWriterFromURL");
ComPtr<IMFMediaType> outputType;
ThrowIfFailed(MFCreateMediaType(&outputType), "MFCreateMediaType(video output)");
ThrowIfFailed(outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video), "SetGUID(output major)");
ThrowIfFailed(outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264), "SetGUID(output subtype H264)");
ThrowIfFailed(outputType->SetUINT32(MF_MT_AVG_BITRATE, videoInfo.bitrate), "SetUINT32(output bitrate)");
ThrowIfFailed(outputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive), "SetUINT32(output interlace)");
ThrowIfFailed(MFSetAttributeSize(outputType.Get(), MF_MT_FRAME_SIZE, videoInfo.width, videoInfo.height), "MFSetAttributeSize(output frame size)");
ThrowIfFailed(MFSetAttributeRatio(outputType.Get(), MF_MT_FRAME_RATE, videoInfo.fpsNum, videoInfo.fpsDen), "MFSetAttributeRatio(output fps)");
ThrowIfFailed(MFSetAttributeRatio(outputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, videoInfo.parNum, videoInfo.parDen), "MFSetAttributeRatio(output PAR)");
ThrowIfFailed(writer->AddStream(outputType.Get(), streamIndex), "AddStream(video)");
ComPtr<IMFMediaType> inputType;
ThrowIfFailed(MFCreateMediaType(&inputType), "MFCreateMediaType(video input)");
ThrowIfFailed(inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video), "SetGUID(input major)");
ThrowIfFailed(inputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12), "SetGUID(input subtype NV12)");
ThrowIfFailed(inputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive), "SetUINT32(input interlace)");
ThrowIfFailed(MFSetAttributeSize(inputType.Get(), MF_MT_FRAME_SIZE, videoInfo.width, videoInfo.height), "MFSetAttributeSize(input frame size)");
ThrowIfFailed(MFSetAttributeRatio(inputType.Get(), MF_MT_FRAME_RATE, videoInfo.fpsNum, videoInfo.fpsDen), "MFSetAttributeRatio(input fps)");
ThrowIfFailed(MFSetAttributeRatio(inputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, videoInfo.parNum, videoInfo.parDen), "MFSetAttributeRatio(input PAR)");
ThrowIfFailed(writer->SetInputMediaType(*streamIndex, inputType.Get(), nullptr), "SetInputMediaType(video)");
ThrowIfFailed(writer->BeginWriting(), "BeginWriting");
return writer;
}
void CopySampleToTopDownBgra(IMFSample* sample, const VideoFormatInfo& videoInfo, std::vector<BYTE>& bgra)
{
ComPtr<IMFMediaBuffer> buffer;
ThrowIfFailed(sample->ConvertToContiguousBuffer(&buffer), "ConvertToContiguousBuffer");
BufferLock lock(buffer.Get());
BYTE* scanline0 = nullptr;
LONG actualStride = 0;
ThrowIfFailed(lock.LockBuffer(videoInfo.sourceStride, videoInfo.height, &scanline0, &actualStride), "LockBuffer");
const size_t dstStride = static_cast<size_t>(videoInfo.width) * 4;
bgra.resize(dstStride * videoInfo.height);
for (UINT32 y = 0; y < videoInfo.height; ++y)
{
const BYTE* srcRow = scanline0 + static_cast<LONG>(y) * actualStride;
BYTE* dstRow = bgra.data() + static_cast<size_t>(y) * dstStride;
std::memcpy(dstRow, srcRow, dstStride);
}
}
void DrawOverlay(std::vector<BYTE>& bgra, UINT32 width, UINT32 height, Gdiplus::Image& overlayImage)
{
const INT stride = static_cast<INT>(width * 4);
Gdiplus::Bitmap frameBitmap(
static_cast<INT>(width),
static_cast<INT>(height),
stride,
PixelFormat32bppRGB,
bgra.data());
ThrowIfGdiplusError(frameBitmap.GetLastStatus(), "Create frame bitmap");
Gdiplus::Graphics graphics(&frameBitmap);
ThrowIfGdiplusError(graphics.GetLastStatus(), "Create graphics");
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
graphics.SetCompositingQuality(Gdiplus::CompositingQualityHighQuality);
graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAliasGridFit);
const Gdiplus::REAL margin = std::max<Gdiplus::REAL>(16.0f, static_cast<Gdiplus::REAL>(height) * kMarginRatio);
const Gdiplus::REAL maxImageW = static_cast<Gdiplus::REAL>(width) * kImageMaxWidthRatio;
const Gdiplus::REAL maxImageH = static_cast<Gdiplus::REAL>(height) * kImageMaxHeightRatio;
const Gdiplus::REAL srcW = static_cast<Gdiplus::REAL>(overlayImage.GetWidth());
const Gdiplus::REAL srcH = static_cast<Gdiplus::REAL>(overlayImage.GetHeight());
if (srcW <= 0.0f || srcH <= 0.0f)
{
throw std::runtime_error("Overlay image has invalid size.");
}
const Gdiplus::REAL imageScale =
std::min<Gdiplus::REAL>(1.0f, std::min(maxImageW / srcW, maxImageH / srcH));
const Gdiplus::REAL drawW = srcW * imageScale;
const Gdiplus::REAL drawH = srcH * imageScale;
Gdiplus::RectF imageRect(margin, margin, drawW, drawH);
Gdiplus::SolidBrush imagePlate(Gdiplus::Color(96, 0, 0, 0));
graphics.FillRectangle(
&imagePlate,
imageRect.X - 8.0f,
imageRect.Y - 8.0f,
imageRect.Width + 16.0f,
imageRect.Height + 16.0f);
graphics.DrawImage(&overlayImage, imageRect);
const Gdiplus::REAL fontPx =
std::max<Gdiplus::REAL>(kMinFontPx, static_cast<Gdiplus::REAL>(height) * 0.06f);
Gdiplus::Font font(L"Segoe UI", fontPx, Gdiplus::FontStyleBold, Gdiplus::UnitPixel);
ThrowIfGdiplusError(font.GetLastStatus(), "Create font");
Gdiplus::StringFormat stringFormat;
stringFormat.SetAlignment(Gdiplus::StringAlignmentNear);
stringFormat.SetLineAlignment(Gdiplus::StringAlignmentNear);
Gdiplus::RectF measureLayout(
margin,
static_cast<Gdiplus::REAL>(height) - margin - fontPx * 2.0f,
static_cast<Gdiplus::REAL>(width) - margin * 2.0f,
fontPx * 2.0f);
Gdiplus::RectF measured;
graphics.MeasureString(kOverlayText, -1, &font, measureLayout, &stringFormat, &measured);
Gdiplus::RectF textBg(
measured.X - 12.0f,
measured.Y - 8.0f,
measured.Width + 24.0f,
measured.Height + 16.0f);
Gdiplus::SolidBrush textPlate(Gdiplus::Color(128, 0, 0, 0));
graphics.FillRectangle(&textPlate, textBg);
Gdiplus::SolidBrush shadowBrush(Gdiplus::Color(220, 0, 0, 0));
Gdiplus::RectF shadowLayout = measureLayout;
shadowLayout.X += 2.0f;
shadowLayout.Y += 2.0f;
graphics.DrawString(kOverlayText, -1, &font, shadowLayout, &stringFormat, &shadowBrush);
Gdiplus::SolidBrush textBrush(Gdiplus::Color(235, 255, 255, 255));
graphics.DrawString(kOverlayText, -1, &font, measureLayout, &stringFormat, &textBrush);
}
void BgraToNv12(const BYTE* bgra, UINT32 width, UINT32 height, BYTE* nv12)
{
const bool useBt709 = (width > 1024 || height > 576);
const int yR = useBt709 ? 47 : 66;
const int yG = useBt709 ? 157 : 129;
const int yB = useBt709 ? 16 : 25;
const int uR = useBt709 ? -26 : -38;
const int uG = useBt709 ? -87 : -74;
const int uB = 112;
const int vR = 112;
const int vG = useBt709 ? -102 : -94;
const int vB = useBt709 ? -10 : -18;
BYTE* yPlane = nv12;
BYTE* uvPlane = nv12 + static_cast<size_t>(width) * height;
const size_t srcStride = static_cast<size_t>(width) * 4;
for (UINT32 y = 0; y < height; ++y)
{
const BYTE* srcRow = bgra + static_cast<size_t>(y) * srcStride;
BYTE* dstY = yPlane + static_cast<size_t>(y) * width;
for (UINT32 x = 0; x < width; ++x)
{
const BYTE b = srcRow[x * 4 + 0];
const BYTE g = srcRow[x * 4 + 1];
const BYTE r = srcRow[x * 4 + 2];
const int Y = ((yR * r + yG * g + yB * b + 128) >> 8) + 16;
dstY[x] = ClampToByte(Y);
}
}
for (UINT32 y = 0; y < height; y += 2)
{
const BYTE* row0 = bgra + static_cast<size_t>(y) * srcStride;
const BYTE* row1 = bgra + static_cast<size_t>(y + 1) * srcStride;
BYTE* dstUV = uvPlane + static_cast<size_t>(y / 2) * width;
for (UINT32 x = 0; x < width; x += 2)
{
int b = 0;
int g = 0;
int r = 0;
for (UINT32 dy = 0; dy < 2; ++dy)
{
const BYTE* row = (dy == 0) ? row0 : row1;
for (UINT32 dx = 0; dx < 2; ++dx)
{
const UINT32 ix = x + dx;
b += row[ix * 4 + 0];
g += row[ix * 4 + 1];
r += row[ix * 4 + 2];
}
}
b = (b + 2) / 4;
g = (g + 2) / 4;
r = (r + 2) / 4;
const int U = ((uR * r + uG * g + uB * b + 128) >> 8) + 128;
const int V = ((vR * r + vG * g + vB * b + 128) >> 8) + 128;
dstUV[x + 0] = ClampToByte(U);
dstUV[x + 1] = ClampToByte(V);
}
}
}
ComPtr<IMFSample> CreateNv12Sample(
const std::vector<BYTE>& bgra,
const VideoFormatInfo& videoInfo,
LONGLONG sampleTime,
LONGLONG sampleDuration)
{
const DWORD bufferSize =
static_cast<DWORD>(videoInfo.width * videoInfo.height * 3 / 2);
ComPtr<IMFMediaBuffer> buffer;
ThrowIfFailed(MFCreateMemoryBuffer(bufferSize, &buffer), "MFCreateMemoryBuffer");
BYTE* dst = nullptr;
DWORD maxLength = 0;
DWORD currentLength = 0;
ThrowIfFailed(buffer->Lock(&dst, &maxLength, ¤tLength), "Lock(NV12 buffer)");
try
{
BgraToNv12(bgra.data(), videoInfo.width, videoInfo.height, dst);
}
catch (...)
{
buffer->Unlock();
throw;
}
ThrowIfFailed(buffer->Unlock(), "Unlock(NV12 buffer)");
ThrowIfFailed(buffer->SetCurrentLength(bufferSize), "SetCurrentLength(NV12 buffer)");
ComPtr<IMFSample> sample;
ThrowIfFailed(MFCreateSample(&sample), "MFCreateSample");
ThrowIfFailed(sample->AddBuffer(buffer.Get()), "AddBuffer(output sample)");
ThrowIfFailed(sample->SetSampleTime(sampleTime), "SetSampleTime");
ThrowIfFailed(sample->SetSampleDuration(sampleDuration), "SetSampleDuration");
return sample;
}
}
int wmain(int argc, wchar_t* argv[])
{
if (argc != 4)
{
std::wcerr << L"Usage: OverlayMp4.exe <input.mp4> <overlayImage.png> <output.mp4>" << std::endl;
return 1;
}
const std::wstring inputPath = argv[1];
const std::wstring imagePath = argv[2];
const std::wstring outputPath = argv[3];
try
{
if (_wcsicmp(inputPath.c_str(), outputPath.c_str()) == 0)
{
throw std::runtime_error("Input and output paths must be different.");
}
ScopedMf mf;
ScopedGdiplus gdiplus;
ComPtr<IMFAttributes> readerAttributes;
ThrowIfFailed(MFCreateAttributes(&readerAttributes, 1), "MFCreateAttributes(reader)");
ThrowIfFailed(
readerAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE),
"SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING)");
ComPtr<IMFSourceReader> reader;
ThrowIfFailed(
MFCreateSourceReaderFromURL(inputPath.c_str(), readerAttributes.Get(), &reader),
"MFCreateSourceReaderFromURL");
VideoFormatInfo videoInfo = ConfigureSourceReader(reader.Get());
if ((videoInfo.width % 2) != 0 || (videoInfo.height % 2) != 0)
{
throw std::runtime_error(
"This sample requires even video width and height because NV12 is 4:2:0.");
}
Gdiplus::Image overlayImage(imagePath.c_str());
ThrowIfGdiplusError(overlayImage.GetLastStatus(), "Load overlay image");
DWORD videoStreamIndex = 0;
ComPtr<IMFSinkWriter> writer =
CreateSinkWriter(outputPath, videoInfo, &videoStreamIndex);
std::vector<BYTE> bgra;
LONGLONG firstTimestamp = -1;
unsigned long long frameCount = 0;
while (true)
{
DWORD flags = 0;
LONGLONG timestamp = 0;
ComPtr<IMFSample> inputSample;
ThrowIfFailed(
reader->ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
0,
nullptr,
&flags,
×tamp,
&inputSample),
"ReadSample(video)");
if ((flags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) != 0)
{
throw std::runtime_error("Dynamic video format change is not supported in this sample.");
}
if ((flags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED) != 0)
{
throw std::runtime_error("Native video format change is not supported in this sample.");
}
if ((flags & MF_SOURCE_READERF_STREAMTICK) != 0)
{
if (firstTimestamp < 0)
{
firstTimestamp = timestamp;
}
ThrowIfFailed(
writer->SendStreamTick(videoStreamIndex, timestamp - firstTimestamp),
"SendStreamTick");
}
if (inputSample)
{
if (firstTimestamp < 0)
{
firstTimestamp = timestamp;
}
LONGLONG duration = 0;
if (FAILED(inputSample->GetSampleDuration(&duration)) || duration <= 0)
{
duration = videoInfo.defaultFrameDuration;
}
CopySampleToTopDownBgra(inputSample.Get(), videoInfo, bgra);
DrawOverlay(bgra, videoInfo.width, videoInfo.height, overlayImage);
ComPtr<IMFSample> outputSample =
CreateNv12Sample(bgra, videoInfo, timestamp - firstTimestamp, duration);
ThrowIfFailed(
writer->WriteSample(videoStreamIndex, outputSample.Get()),
"WriteSample(video)");
++frameCount;
}
if ((flags & MF_SOURCE_READERF_ENDOFSTREAM) != 0)
{
break;
}
}
ThrowIfFailed(writer->Finalize(), "Finalize");
std::wcout
<< L"Done. frames=" << frameCount
<< L", output=" << outputPath
<< std::endl;
return 0;
}
catch (const std::exception& ex)
{
std::cerr << ex.what() << std::endl;
return 1;
}
}
7. النقاط التي يجب الإمساك بها عند قراءة هذا التنفيذ
7.1 الصيغة السهلة للرسم تختلف عن الصيغة التي يستقبلها الـ encoder
في هذه العيّنة،
- مخرج
Source Reader:RGB32 - الرسم:
GDI+ - مدخل
Sink Writer:NV12
يتمّ اعتماد هذا التدفّق.
السبب بسيط: عائلة RGB أسهل تعاملاً عند وضع النصّ أو PNG، وNV12 أسهل تعاملاً عند تمريره إلى ترميز H.264.
عند قراءة التنفيذ، فإنّ تقسيمه إلى «مرحلة الرسم» و«مرحلة التهيئة قبل الترميز» يجعل المتابعة أسهل.
7.2 يتمّ امتصاص stride والاتّجاه الرأسيّ أوّلاً قبل الرسم
إطارات الفيديو ليست بالضرورة مرتّبةً في الذاكرة بالشكل المرئيّ.
- قد لا يساوي stride القيمة
width * 4 - قد يكون الاتّجاه الرأسيّ مقلوباً
- معاملة
IMF2DBufferوIMFMediaBufferمختلفة قليلاً
لذا فإنّ هذا الكود يطبّع أوّلاً إلى buffer BGRA من نوع top-down ثمّ يرسم.
بعد ضبط هذا الجزء أوّلاً، يصبح كود جانب الرسم سلساً جدّاً.
7.3 لا يكفي النظر في HRESULT فقط في ReadSample، بل أيضاً في flags وsample
قد يعيد ReadSample قيمة S_OK مع كون sample == nullptr.
الأمثلة النموذجيّة هي:
MF_SOURCE_READERF_STREAMTICKMF_SOURCE_READERF_ENDOFSTREAM- وأحداث stream أخرى
لذلك في الحلقة، يجب النظر معاً في HRESULT وflags وinputSample الثلاثة.
خصوصاً إن تجاهلت STREAMTICK وENDOFSTREAM، فإنّ معالجة الـ timeline في المرحلة التالية تنهار بسهولة.
7.4 من الأكثر أماناً وراثة timestamp وduration من المدخل
الـ timestamp يُقاس بوحدات 100ns.
كما أنّه يجب الحصول على duration بشكل منفصل من IMFSample.
بدلاً من فرض زيادة محسومة في كلّ مرّة بافتراض fps ثابت، فإنّ وراثة timestamp / duration من sample المدخل قدر الإمكان نهج أقلّ عرضةً للانهيار.
في هذه العيّنة أيضاً، نلجأ إلى القيمة الافتراضيّة المحسوبة من fps فقط عند تعذّر الحصول على duration.
7.5 GDI+ خفيف الإدخال، لكن للفيديوهات الطويلة والدقّة العالية مرحلة تالية
GDI+ ملائم جدّاً لعيّنة بملفّ واحد كامل، لكن في معالجة كميّات كبيرة من الفيديوهات الطويلة أو 4K، قد يكون D3D11 + Direct2D + DirectWrite أفضل أحياناً.
- مرّر الكلّ أوّلاً عبر
GDI+ - ثمّ استبدله بـ
Direct2D / DirectWriteعند الحاجة - انقل تحويل الألوان إلى
Video Processor MFTأو إلى جانب GPU
اعتماد هذا التدرّج يجعل التوسيع أسهل دون كسر التصميم.
7.6 هذه العيّنة محصورة في الفيديو فقط
تكديس الصوت أيضاً في نفس المقال يُشتّت محور النقاش بسهولة.
لذلك تركّز هذه العيّنة على دمج الصورة والنصّ في إطار الفيديو، وتجعل الإخراج MP4 يحتوي على الفيديو فقط.
في الممارسة العمليّة، فإنّ الامتداد إلى البنية التالية في المرحلة التالية أسهل:
- الفيديو فقط:
Source Reader -> الدمج -> Sink Writer - الصوت: remux مضغوط كما هو
8. عندما لا تكون «بيانات الفيديو المُعطاة» ملفّاً بل سلسلة بايتات MP4 في الذاكرة
الكود في هذه المرّة يستخدم MFCreateSourceReaderFromURL، فيكون الإدخال مسار ملفّ.
لكن إن كان الطلب «أريد القيام بنفس العمل على سلسلة بايتات mp4 وصلتني عبر API»، فإنّ الفكرة لا تتغيّر.
ما يتغيّر هو المدخل فقط.
- جهّز
IStreamأو stream مخصّصاً - مرّره إلى
Source Readerعلى شكلIMFByteStream - بعد ذلك يكون نفس التدفّق
RGB32 -> الرسم -> NV12 -> Sink Writer
أي أنّ الجوهر هو ليس كيفيّة احتفاظ بيانات الفيديو، بل كيفيّة الرسم على كلّ إطار بعد فكّ التشفير.
9. عند التوسيع نحو الإنتاج
9.1 إضافة remux الصوت
أوّل توسعة عمليّة وأكثرها واقعيّةً هي الإبقاء على الصوت كما هو.
بنية إعادة ترميز الفيديو فقط وكتابة الصوت بنفس صيغته المضغوطة كما هو يلبّي المتطلّبات دون زيادة كبيرة في حجم التنفيذ.
9.2 إدراج Video Processor MFT
عيّنة هذه المرّة تنفّذ BGRA -> NV12 يدويّاً إعطاءً للأولويّة لاكتمال الملفّ الواحد، لكن في الإنتاج، فإنّ بنية إدراج Video Processor MFT خيار قويّ جدّاً.
عند استخدام Video Processor MFT، يصبح من الأسهل التعامل دفعةً واحدةً مع:
- تحويل فضاء الألوان
- تغيير الحجم
- إلغاء التشابك
- تحويل معدّل الإطارات
9.3 استبدال GDI+ بـ Direct2D / DirectWrite
طبقات overlay مثل صور الشعار والترجمات والـ timestamps كافية بـ GDI+ في كثير من الحالات، لكن لعصر الأداء فإنّ Direct2D / DirectWrite أفضل.
خصوصاً إن وُجدت شروط مثل:
- دقّة عالية
- مدّة طويلة
- عدد كبير من الملفّات
- الرغبة المستقبليّة في الانتقال إلى مسار GPU
فإنّ بنية تستخدم D3D11 / DXGI surface تدخل في حقل الرؤية.
9.4 يُنظر في custom MFT عندما يصبح «مؤثّر فيديو يُراد إعادة استخدامه»
في Media Foundation يمكن تنفيذ المؤثّرات على شكل IMFTransform.
لذلك إن أردت إعادة استخدام نفس معالجة الـ overlay عبر تطبيقات أو خطوط أنابيب متعدّدة، فإنّ custom MFT خيار أنيق.
لكن كأوّل تنفيذ، فإنّ:
- الالتزام بعقد
IMFTransformضروريّ - إدارة أنواع الوسائط للمدخل والمخرج تتزايد
- ترتفع صعوبة التسجيل وتصحيح الأخطاء
لذلك في الممارسة العمليّة كثيراً ما يكون الأسهل تشغيل Source Reader + الدمج + Sink Writer بشكل صحيح أوّلاً، ثمّ استخراجه كـ MFT عند الحاجة.
10. الخلاصة
عند دمج صورة أو نصّ في جميع إطارات فيديو MP4 باستخدام Media Foundation، فإنّ التفكير وفق التقسيم التالي يسهّل الترتيب.
- الاستخراج:
IMFSourceReader - الرسم:
GDI+أوDirect2D / DirectWrite - التهيئة لما يستقبله الـ encoder بسهولة:
NV12ونحوها - إعادة الكتابة:
IMFSinkWriter
وإن كنت تريد «عيّنةً تلصقها كاملةً في .cpp واحد فتعمل مباشرةً»، فإنّ بنية هذه المرّة:
Source Reader -> RGB32 -> صورة + HelloWorld بـ GDI+ -> BGRA to NV12 -> Sink Writer
بنية سلسة جدّاً.
عند التوسّع نحو الإنتاج بعدها، فإنّ التفكير وفق هذا الترتيب أقلّ عرضةً للانهيار:
- إضافة remux الصوت
- استبدال
GDI+بـDirect2D / DirectWrite - نقل تحويل
NV12إلىVideo Processor MFTأو إلى جانب GPU - التقدّم إلى بنية قائمة على
D3D11 surfaceللفيديوهات الطويلة وعالية الدقّة - استخراجه كـ custom
MFTإن لزمت قابليّة إعادة الاستخدام
تكديس كلّ شيء دفعةً واحدةً يجعل COM وstride وفضاء الألوان وإدارة الـ surfaces تنهال عليك معاً.
البدء بتمرير المراحل واحدةً واحدةً أوّلاً ثمّ تقوية ما يلزم لاحقاً يجعل التصميم وتصحيح الأخطاء أسهل بكثير.
11. مقالات ذات صلة من شركة كومورا سوفت ذ.م.م.
- ما هو Media Foundation - لماذا يبدأ في الإحساس بأنّه COM وواجهات Windows الإعلاميّة في آنٍ واحد
- كيفيّة استخراج صورة ثابتة من MP4 باستخدام Media Foundation - ملفّ .cpp واحد يمكن لصقه في تطبيق وحدة تحكّم C++
12. مراجع
- Microsoft Learn: Using the Source Reader to Process Media Data
- Microsoft Learn: MFCreateSourceReaderFromByteStream
- Microsoft Learn: MFCreateMFByteStreamOnStream
- Microsoft Learn: IMFSourceReader::SetCurrentMediaType
- Microsoft Learn: MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING
- Microsoft Learn: MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING
- Microsoft Learn: IMFSourceReader::ReadSample
- Microsoft Learn: Working with Media Samples
- Microsoft Learn: IMF2DBuffer::Lock2D
- Microsoft Learn: Video Subtype GUIDs
- Microsoft Learn: H.264 Video Encoder
- Microsoft Learn: Video Processor MFT
- Microsoft Learn: Using the Sink Writer
- Microsoft Learn: Tutorial: Using the Sink Writer to Encode Video
- Microsoft Learn: Interoperability Overview (Direct2D)
- Microsoft Learn: Text Rendering with Direct2D and DirectWrite
- Microsoft Learn: Writing a Custom MFT
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
كيفيّة تحويل إطارات YUV إلى RGB باستخدام Media Foundation - أنماط التحويل التلقائيّ في Source Reader والتحويل اليدويّ
دليل عمليّ لتحويل إطارات YUV إلى RGB في Media Foundation: التحويل التلقائيّ عبر Source Reader مقابل التحويل اليدويّ لـ NV12 و YUY2 مع col...
كيفيّة استخراج صورة ثابتة من MP4 باستخدام Media Foundation - ملفّ .cpp واحد يمكن لصقه في تطبيق وحدة تحكّم C++
دليل عمليّ لاستخراج إطار ثابت قرب لحظة محدّدة من ملفّ MP4 عبر Source Reader في Media Foundation، مع معالجة الـ seek و stride واتّجاه الصو...
ما هو 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++ ب...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة