كيف تحرق صورة ونصّاً في إطارات MP4 بـ Media Foundation
· آخر تحديث: · 小村 豪 · Media Foundation, C++, تطوير Windows, GDI+, Direct2D, DirectWrite, H.264
سجل التعديلات (3 تحديثات، آخر تحديث 3 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240854)
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621398)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
小村 豪 (2026). كيف تحرق صورة ونصّاً في إطارات MP4 بـ Media Foundation. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621398 https://comcomponent.com/ar/blog/2026/03/16/009-media-foundation-overlay-image-text-on-mp4-frames/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621398
- DOI (هذه النسخة)
- 10.5281/zenodo.22279815
علامة مائية للشعار، نتيجة الفحص، رقم الجهاز، اسم المشغّل، طابع الزمن. متطلّب إنتاج MP4 جديد بعد حرق معلومات كهذه في كل إطارات فيديو MP4 شائع جداً في واجهات المراقبة والفحص والأثر والتحليل.
غير أنك ما إن تبدأ لمس Media Foundation حتى تتراصف IMFSourceReader وIMFSample وIMFMediaBuffer وIMFTransform وIMFSinkWriter، فيصعب فجأة رؤية أين تضع النص أو PNG في النهاية.
ترتّب هذه المقالة أولاً الصورة الكاملة Source Reader -> الرسم -> تحويل الألوان -> Sink Writer، ثم تضع بعد ذلك عيّنة مكتملة في ملف واحد تُلصق كما هي في تطبيق وحدة تحكّم C++ في Visual Studio.
العيّنة تقرأ MP4 محدّداً، وترسم على كل إطار صورة محدّدة وHelloWorld، وتنتج MP4 ناتجاً.
لاحظ أن هذه العيّنة تقدّم اللصق والتشغيل كما هما أولاً، فالتكوين إعادة ترميز الفيديو فقط. يمكن حشر remux الصوت في الملف نفسه، لكن موضوع المقالة «حرق صورة ونص في كل إطار»، لذا نضيّق هناك أولاً.
flowchart TB
accTitle: كيف تضيّق هذه العيّنة
accDescr: يبين المخطّط أن عيّنة المقالة تقدّم اللصق والتشغيل كما هما أولاً فتعيد ترميز الفيديو فقط، وأن remux الصوت يُؤجَّل إلى توسّع بعد أمرار الموضوع وهو الحرق.
fo1["قدّم اللصق والتشغيل أولاً"] --> fo2["أعد ترميز الفيديو فقط"]
fo2 --> fo3["ضيّق على الحرق في كل إطار"]
fo1 -.-> fo4["أجّل remux الصوت إلى توسّع لاحق"]
الشكل 1: أوّل نسخة تكوين أدنى يمرّ فيه موضوع الحرق وحده.
شيفرة هذه المقالة منشورة على GitHub كطقم عيّنة (ملف .cpp مكتمل وبنية بناء CMake).
media-foundation-overlay-image-text-on-mp4-frames - komurasoft-blog-samples (GitHub)
الجمهور المستهدف والبيئة اللازمة
المقالة موجّهة إلى المطوّر متوسط المستوى الذي سيكتب الآن معالجة فيديو Windows بـ C++. نفترض أنك لمست أساسيات COM (ComPtr، HRESULT، عدّ المراجع)، وأن Media Foundation مرحلة قادمة.
البيئة اللازمة لتشغيل العيّنة كالتالي. التفاصيل وشروط بيانات الإدخال مجموعة في الفصل 5.
| البند | الافتراض |
|---|---|
| نظام التشغيل | Windows 10 / 11 |
| بيئة التطوير | تطبيق وحدة تحكّم C++ في Visual Studio 2022 |
| تكوين البناء | x64 |
| الترويسة المترجمة مسبقاً | اجعل إعداد هذا .cpp بلا استخدامها |
| فيديو الإدخال | MP4 عادي. العرض والارتفاع زوجيان (لأن NV12 بصيغة 4:2:0) |
| الخرج | MP4 فيديو فقط. لا صوت معه |
مصطلحات ينبغي تثبيتها مسبقاً
من جدول الفصل 3 تظهر إنجليزية بلا شرح. نكتب سطراً لكل منها مسبقاً.
| المصطلح | المعنى |
|---|---|
| remux | إعادة صنع الوعاء (الحاوية) مع الإبقاء على بيانات الضغط كما هي. بلا إعادة ترميز فلا تتدهور الصورة ولا الصوت، والمعالجة خفيفة |
| topology | مخطّط يعبّر به Media Foundation عن «من أي جزء إلى أي جزء تسري البيانات». صورة تكوين تربط المصدر والتحويل والمصب |
| custom MFT | Media Foundation Transform تكتبه بنفسك. تنفيذ IMFTransform يتيح إدراج المؤثّر جزءاً في خط أنابيب Media Foundation |
| stride | عدد البايتات التي يشغلها صف صورة واحد في الذاكرة. لا يطابق حتماً العرض × 4، وقد يدخل فراغ في نهاية الصف |
1. الخلاصة أولاً
- الشكل الأساسي لإدخال صورة أو نص في كل إطار من MP4 هو
فك التشفير بـ Source Reader -> الدمج على إطار غير مضغوط -> تحويل الألوان إن لزم -> إعادة الترميز بـ Sink Writer. - وضع الصورة أو النص نفسه ليس عمل Media Foundation. هنا أصدق التفكير بواجهات رسم مثل
GDI+وDirect2DوDirectWriteوWIC. - إن عدت إلى
MP4(H.264)لزم غالباً مرحلة تحويل تربط بينRGB32 / ARGB32الأسهل للرسم وNV12 / I420 / YUY2الذي يتقبّله المرمّز بسهولة. - لتشغيل أوّل نسخة، تكوين
Source Reader -> RGB32 -> الرسم بـ GDI+ -> NV12 -> Sink Writerواضح. - إن قدّمت السرعة وقابلية التوسّع، فالميل إلى
D3D11 / DXGI surface -> Direct2D / DirectWrite -> Video Processor MFT -> Sink Writerيفتح مجالاً للنمو.
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 20، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. لماذا هذه المسألة معقّدة قليلاً
«إدخال نص في الفيديو» يخلط في الواقع أربعة أحاديث.
-
حديث الحاوية والمرمّز
mp4حاوية وليست الإطار نفسه. المحتوى غالباً بيانات مضغوطة بـH.264أوH.265. -
حديث فك التشفير / الترميز على البيانات المضغوطة كما هي لا تضع نصّاً أو PNG بواجهة رسم ثنائية الأبعاد عادية. يلزم أولاً العودة إلى إطار غير مضغوط.
-
حديث الرسم النص، والشعار، ودمج شفافية PNG، ورسم النص مع تنعيم الحواف ليست دور Media Foundation نفسه. هذا عمل
GDI+أوDirect2D / DirectWrite / WIC. -
حديث فضاء الألوان وصيغة البكسل الصيغة الأسهل للرسم والصيغة التي يفضّلها المرمّز لا تتطابقان. هنا موضع تعثّر هادئ.
بكلمة فجّة، الأسهل ترتيباً التفكير ليس «إدخال نص بـ Media Foundation» بل «تدوير الإطارات بـ Media Foundation، والوضع بواجهة رسم، وإدخال تحويل الألوان اللازم ثم الترميز».
flowchart TB
accTitle: أربعة أحاديث مختلطة
accDescr: يبين المخطّط أن متطلّب إدخال نص في الفيديو يخلط أربعة أحاديث: الحاوية والمرمّز، وفك التشفير والترميز، والرسم، وفضاء الألوان وصيغة البكسل.
mixq["إدخال نص في الفيديو"] --> t1["حديث الحاوية والمرمّز"]
mixq --> t2["حديث فك التشفير والترميز"]
mixq --> t3["حديث الرسم"]
t3 -.-> t4["حديث فضاء الألوان وصيغة البكسل"]
الشكل 2: إن تعثّرت فافصل أولاً في أي حديث أنت الآن.
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[إطار غير مضغوط<br/>RGB32]
C --> D[رسم الصورة + HelloWorld بـ GDI+]
D --> E[تحويل BGRA -> NV12]
E --> F[IMFSinkWriter]
F --> G[output.mp4]
B --> H[عيّنة صوت]
H --> I[نسخ كما هي<br/>أو إعادة ترميز]
I --> F
الشكل 3: استخرج بـ Source Reader، ارسم بـ GDI+، حوّل إلى NV12، واكتب بـ Sink Writer.
المهم هنا أن الرسم نفسه ليس عمل Media Foundation. Media Foundation مسؤولة عن إدخال الإطارات وإخراجها، ووضع الصورة والنص يُترك لواجهة الرسم.
4. كيف تقسم خط الأنابيب في التفكير
4.1 الإدخال يُستقبل بـ IMFSourceReader
إن كان الإدخال مسار ملف فالتكوين الواضح MFCreateSourceReaderFromURL، وإن كانت بيانات فيديو في الذاكرة فاصنع IMFByteStream واستخدم MFCreateSourceReaderFromByteStream.
ما ينبغي تقريره أولاً هنا هل تستقبل بصيغة أسهل للرسم أم بصيغة موجّهة إلى المرمّز.
- إن أردت تبسيط التنفيذ فـ
RGB32أوARGB32 - إن قدّمت كفاءة الترميز فـ YUV مثل
NV12
غير أن دمج النص أو PNG أسهل تفكيراً بكثير في عائلة RGB، لذا أسهل أن تستقبل في الخطوة الأولى بـ RGB32 / ARGB32.
flowchart TB
accTitle: الخطوة الأولى لأي صيغة تستقبل
accDescr: يبين المخطّط أن تبسيط التنفيذ يعني الاستقبال بـ RGB32 أو ARGB32، وتقديم كفاءة الترميز يعني الاستقبال بـ YUV مثل NV12، لكن دمج النص أو PNG أسهل تفكيراً في عائلة RGB لذا أسهل الاستقبال بعائلة RGB في الخطوة الأولى.
rq1{"ماذا تقدّم؟"}
rq1 -->|"سهولة التنفيذ"| rf1["استقبل بـ RGB32 / ARGB32"]
rq1 -->|"كفاءة الترميز"| rf2["استقبل بـ YUV مثل NV12"]
rf1 -.-> rf3["الدمج أسهل تفكيراً في عائلة RGB"]
الشكل 4: إن تحيّرت فقدّم سهولة الرسم وابدأ الاستقبال بعائلة RGB.
تفعيل MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING يجعل Source Reader يحوّل YUV -> RGB32 ويزيل التداخل.
هذا مريح في مرحلة «أريد استخراج الإطار والتعامل معه أولاً»، لكنه يثقل بسهولة في الفيديو الطويل أو العالي الدقة، فإن لزمت السرعة في الإنتاج استحق التكوين مراجعة لاحقاً.
flowchart TB
accTitle: ربح ENABLE_VIDEO_PROCESSING وخسارته
accDescr: يبين المخطّط أن تفعيل MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING يجعل Source Reader يتولّى التحويل من YUV إلى RGB32 وإزالة التداخل، لكنه يثقل بسهولة في الفيديو الطويل أو العالي الدقة، فإن لزمت السرعة في الإنتاج استحق التكوين مراجعة.
ev1["فعّل العلامة"] --> ev2["أوكل التحويل من YUV إلى RGB32"]
ev1 --> ev3["أوكل أيضاً إزالة التداخل"]
ev2 -.-> ev4["يثقل بسهولة في الطويل والعالي الدقة"]
الشكل 5: علامة مريحة تسهّل الاستخراج لها ثمن في السرعة.
4.2 دمج الصورة والنص يُفكَّر فيه بـ GDI+ أو Direct2D / DirectWrite
تستخرج المخزن من IMFSample الذي استقبلته من Media Foundation، وتضع فوقه صورة شعار أو نصّاً.
عيّنة هذه المرّة تقدّم سهولة اللصق مكتملاً في ملف واحد فتستخدم GDI+ للرسم.
- تستطيع قراءة الصورة
- تستطيع رسم النص
- الإعداد الإضافي قليل نسبياً
- يسهل حصرها في
.cppواحد لتطبيق وحدة التحكّم
أما في الفيديو الطويل أو معالجة 4K بكميات كبيرة فمجال النمو أوفر مع D3D11 + Direct2D + DirectWrite.
طبيعي أن يكون المسار GDI+ في التنفيذ الأوّل، ثم الانتقال إلى Direct2D / DirectWrite عند ضغط السرعة.
4.3 ليس مضموناً أن تكتب RGB32 كما هو إلى H.264
هنا أكثر موضع تعثّر.
عند العودة إلى MP4(H.264) يفترض مرمّز H.264 من Microsoft غالباً مدخلاً من عائلة YUV مثل I420 / IYUV / NV12 / YUY2 / YV12.
أي ليس مضموناً أن تنتهي بعد الدمج بصيغة RGB32 / ARGB32 الأسهل للرسم برميها كما هي إلى IMFSinkWriter.
لذا يلزم في التنفيذ أحد التحويلين.
- إدراج
Video Processor MFTلتحويلRGB32 / ARGB32 -> NV12 - إدخال تحويل
RGB -> NV12خاص بك
عيّنة هذه المرّة تقدّم الاكتمال في ملف واحد فتدخل التحويل الخاص وهو الثاني.
في الإنتاج تكوين إدراج Video Processor MFT الذي يعالج تحويل فضاء الألوان وتغيير الحجم وإزالة التداخل دفعة خيار قوي أيضاً.
flowchart TB
accTitle: طريقان يربطان RGB بـ NV12
accDescr: يبين المخطّط أنه بعد الدمج بصيغة RGB32 أو ARGB32 الأسهل للرسم يلزم إمّا التحويل بإدراج Video Processor MFT أو إدخال تحويل خاص من RGB إلى NV12، وأن هذه العيّنة تقدّم الاكتمال في ملف واحد فتختار التحويل الخاص.
cv1["اكتمل الدمج بعائلة RGB"] --> cv2["حوّل بـ Video Processor MFT"]
cv1 --> cv3["حوّل إلى NV12 بنفسك"]
cv3 -.-> cv4["العيّنة تقدّم الاكتمال في ملف واحد"]
cv2 -.-> cv5["تكوين قوي في الإنتاج"]
الشكل 6: افترض أن مرحلة التحويل لازمة حتماً، واختر فقط أين تحملها.
4.4 الخرج يُكتب بـ IMFSinkWriter
خرج الفيديو يسهل التعامل معه بـ IMFSinkWriter.
التفكير بسيط:
- نوع تدفق الخرج … الصيغة التي تريد كتابتها في الملف
مثال:
MFVideoFormat_H264 - نوع تدفق الإدخال … الصيغة التي يمرّرها التطبيق إلى
Sink Writerمثال:MFVideoFormat_NV12
تُضبطان منفصلتين.
أي من منظور Sink Writer:
- جانب التطبيق يمرّر إطاراً غير مضغوط بصيغة
NV12 Sink Writerيرمّزه إلى H.264 ويكتبه في MP4
هذه العلاقة.
flowchart TB
accTitle: تقسيم نوعي إدخال Sink Writer وخرجه
accDescr: يبين المخطّط أن Sink Writer يُضبط له NV12 نوع تدفق إدخال يمرّره التطبيق، وH.264 نوع تدفق خرج يُكتب في الملف، منفصلين، ويتولّى Sink Writer الترميز والكتابة إلى MP4.
ap1["التطبيق يمرّر إطار NV12"] --> sw1["Sink Writer"]
sw1 --> sw2["الترميز إلى H.264"]
sw2 --> sw3["الكتابة إلى MP4"]
sw1 -.-> sw4["اضبط نوع الإدخال ونوع الخرج منفصلين"]
الشكل 7: ممارسة Sink Writer ضبط الصيغة الممرَّرة والصيغة المكتوبة منفصلتين.
4.5 التفكير في الصوت منفصلاً أولاً يسهّل الترتيب
كثير جداً أن تريد إدخال شعار أو نص في الفيديو دون تغيير الصوت نفسه.
عملياً يسهل استخدام تكوين:
- تدفق الفيديو فقط
Source Reader -> الدمج -> Sink Writer - تدفق الصوت remux مضغوطاً كما هو
غير أن عيّنة هذه المرّة تركّز على حرق صورة ونص في الإطار، فالخرج MP4 فيديو فقط. نسخة تُبقي الصوت أسهل تتبعاً إن أُضيفت في مرحلة التوسّع بعد ذلك.
flowchart TB
accTitle: فكّر في الفيديو والصوت منفصلين
accDescr: يبين المخطّط أن التكوين العملي يمرّر تدفق الفيديو وحده من Source Reader عبر الدمج إلى Sink Writer، ويعمل remux لتدفق الصوت مضغوطاً كما هو، وأن هذه العيّنة تضيّق التركيز فالخرج فيديو فقط.
vs1["تدفق الفيديو"] --> vs2["ادمج ومرّر إلى Sink Writer"]
as1["تدفق الصوت"] --> as2["remux مضغوطاً كما هو"]
as2 -.-> as3["العيّنة لا تعالجه"]
الشكل 8: إن كان ما تريد تغييره الفيديو فقط فانقل الصوت في وعائه دون لمسه.
5. افتراضات هذه العيّنة وطريقة استخدامها
افتراضات هذه الشيفرة كالتالي.
- Windows 10 / 11
- تطبيق وحدة تحكّم C++ في Visual Studio 2022
- بناء
x64 - ملف
.cppهذا لا يستخدم ترويسة مترجمة مسبقاً - عرض فيديو الإدخال وارتفاعه زوجيان
- الإدخال ملف فيديو MP4 عادي
- الخرج MP4 فيديو فقط
- الصورة صيغة يقرأها GDI+ مثل PNG / JPEG / BMP / GIF
NV12 بصيغة 4:2:0 لذا يلزم أن يكون العرض والارتفاع زوجيين.
لذلك توقّف هذه العيّنة بخطأ صريح إن لم يُستوف الشرط.
flowchart TB
accTitle: افتراض أن العرض والارتفاع زوجيان
accDescr: يبين المخطّط أن NV12 عيّنة فرعية 4:2:0 فيلزم أن يكون عرض فيديو الإدخال وارتفاعه زوجيين، وأن هذه العيّنة توقّف بخطأ صريح إن لم يُستوف الشرط.
nvq1["NV12 بصيغة 4:2:0"] --> nvq2["يلزم عرض وارتفاع زوجيان"]
nvq2 --> nvq3{"هل الإدخال زوجي؟"}
nvq3 -->|"زوجي"| nvq4["تابع المعالجة"]
nvq3 -->|"يتضمّن فردياً"| nvq5["توقّف بخطأ صريح"]
الشكل 9: بدل الانهيار صامتاً أوقف الإدخال الذي لا يستوفي الافتراض عند المدخل.
5.1 طريقة الاستخدام
- اصنع Console App في Visual Studio
- الصق هذا
.cppكلّه - اجعل الترويسة المترجمة مسبقاً لـ
.cppذلك «لا تُستخدم» - ابنِ بـ
x64 - شغّل كالتالي
OverlayMp4.exe input.mp4 overlay.png output.mp4
input.mp4الفيديو الأصليoverlay.pngالصورة المراد وضعهاoutput.mp4جهة الخرج
السلسلة ثابتة HelloWorld في kOverlayText عند رأس الشيفرة.
الموضع والحجم يُغيَّران بلمس الثوابت داخل الشيفرة. التعديل ليصير التمرير وسيط سطر أوامر موضوع في 9.5.
5.2 التحقق من أن التشغيل صحيح
«انتهى بلا خطأ» شيء و«حُرق صحيحاً» شيء آخر. النظر إلى الأربعة التالية بالترتيب يجد معظم الإخفاق.
- انظر إلى عدد الإطارات عند الإنهاء. تخرج هذه العيّنة عند انتهاء المعالجة عدد الإطارات المكتوبة بعد
Done. frames=. إن انزاح كثيراً عن إجمالي إطارات فيديو الإدخال فقد أسقطت الحلقة شيئاً في موضع ما منReadSample - قارن المعلومات الأساسية لملف الخرج بالإدخال. انقر باليمين على MP4 الخرج في المستكشف، وافتح الخصائص > التفاصيل، فيظهر الطول وعرض الإطار وارتفاعه ومعدّل الإطارات. إن لم يتطابق الطول مع الإدخال فاشك في التعامل مع timestamp (7.4)
- انظر بالعين إلى ثلاثة مواضع: الرأس والوسط والنهاية. الاطمئنان بإطار الرأس وحده يفوّت عيباً تختفي فيه الطبقة في المنتصف. الأضمن قص صورة ثابتة من الإدخال والخرج عند اللحظة نفسها وصفّهما، والإجراءات في «كيفية استخراج صورة ثابتة من MP4 باستخدام Media Foundation»
- انظر إن كان اللون غريباً. إن صار لون بشرة الإنسان أو السماء غير طبيعي فقد لا يطابق اختيار معاملات
BgraToNv12(BT.601 وBT.709) الإدخال
عند التجربة الأولى يُوصى بـ MP4 قصير من ثوانٍ وPNG واضحة المحيط. محاولة أمرار أوّل نسخة بفيديو طويل تطيل فصل المشكلة.
flowchart TB
accTitle: إجراءات التحقق من أن التشغيل صحيح
accDescr: يبين المخطّط أن الانتهاء بلا خطأ والحرق الصحيح شيئان مختلفان، لذا يُنظر بالترتيب إلى مطابقة عدد الإطارات، ومقارنة المعلومات الأساسية لملف الخرج، والنظر بالعين إلى الرأس والوسط والنهاية، وتأكيد شذوذ اللون.
ck1["طابق عدد الإطارات مع الإدخال"] --> ck2["قارن الطول والحجم في الخصائص"]
ck2 --> ck3["انظر بالعين إلى الرأس والوسط والنهاية"]
ck3 --> ck4["أكّد عدم طبيعية اللون"]
ck4 -.-> ck5["إن شذ فاشك في اختيار المعاملات"]
الشكل 10: بلا خطأ والصحة شيئان، فأكّد بالترتيب من أربعة جوانب.
6. شيفرة مكتملة في ملف واحد تُلصق في .cpp
6.1 خريطة الشيفرة
نضع الخريطة أولاً. الشيفرة طويلة، لكن المركز الذي ينبغي قراءته فعلاً ثلاثة: CopySampleToTopDownBgra وDrawOverlay وBgraToNv12، وحلقة wmain فقط. الباقي تهيئة وتنظيف.
| الدالة / الصنف | الدور | الشرح المفصّل |
|---|---|---|
ScopedMf / ScopedGdiplus |
مقابلة تهيئة MFStartup وGDI+ وإنهائهما بـ RAII |
— |
ConfigureSourceReader |
ضبط خرج Source Reader على RGB32 واستخراج العرض والارتفاع وfps وframe duration الافتراضي |
4.1 |
GetDefaultStride |
طلب stride الافتراضي من نوع الوسائط | 7.2 |
BufferLock |
قفل المخزن بـ IMF2DBuffer إن وُجد، وإلا بـ IMFMediaBuffer |
7.2 |
CopySampleToTopDownBgra |
امتصاص stride واتجاه الأعلى/الأسفل والتطبيع إلى مخزن BGRA من نوع top-down | 7.2 |
DrawOverlay |
رسم الصورة والنص بـ GDI+. هذه وحدها «مرحلة الرسم» | 4.2 / 7.1 |
BgraToNv12 |
تحويل BGRA بعد الرسم إلى NV12 | 4.3 / 7.1 |
CreateNv12Sample |
لف مخزن NV12 في IMFSample وإرفاق timestamp وduration |
7.4 |
ChooseBitrate |
تقرير معدّل بت الخرج من معلومات الإدخال | — |
CreateSinkWriter |
ضبط نوع H.264 في جانب الخرج ونوع NV12 الذي نمرّره |
4.4 |
حلقة while في wmain |
تدوير إطار إطار مع النظر إلى HRESULT / flags / sample في ReadSample |
7.3 / 7.4 |
مقابلة صورة معالجة الفصل 3: CopySampleToTopDownBgra يقابل «الإطار غير المضغوط»، وDrawOverlay «الرسم بـ GDI+»، وBgraToNv12 «التحويل من BGRA إلى NV12».
flowchart TB
accTitle: الدوال الثلاث المركزية التي ينبغي قراءتها
accDescr: يبين المخطّط أن المركز الذي ينبغي قراءته فعلاً في الشيفرة الطويلة ثلاثة CopySampleToTopDownBgra وDrawOverlay وBgraToNv12 وحلقة wmain، وتقابل التطبيع إلى إطار غير مضغوط والرسم والتحويل إلى NV12.
lp1["حلقة wmain تدور إطاراً إطاراً"] --> fn1["CopySampleToTopDownBgra"]
fn1 --> fn2["DrawOverlay"]
fn2 --> fn3["BgraToNv12"]
fn1 -.-> ro1["التطبيع إلى إطار غير مضغوط"]
fn2 -.-> ro2["رسم الصورة والنص"]
fn3 -.-> ro3["التهيئة للترميز"]
الشكل 11: الباقي تهيئة وتنظيف، والمركز هذه الدوال الثلاث والحلقة فقط.
6.2 الشيفرة كاملة
#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);
for (UINT32 x = 0; x < videoInfo.width; ++x)
{
dstRow[static_cast<size_t>(x) * 4 + 3] = 0xFF;
}
}
}
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,
PixelFormat32bppPARGB,
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 الصيغة الأسهل للرسم والصيغة التي يتقبّلها المرمّز شيءان مختلفان
في هذه العيّنة التدفّق:
- خرج
Source Reader:RGB32 - الرسم:
GDI+ - إدخال
Sink Writer:NV12
السبب بسيط: وضع النص أو PNG أسهل تعاملاً في عائلة RGB، والتمرير إلى ترميز H.264 أسهل تعاملاً بـ NV12.
عند قراءة التنفيذ أسهل التتبع إن فصلت هنا «مرحلة الرسم» عن «مرحلة التهيئة قبل الترميز».
7.2 يُمتص stride واتجاه الأعلى/الأسفل أولاً ثم يُرسم
إطار الفيديو لا يتراصف في الذاكرة حتماً كما يبدو.
- قد لا يطابق stride القيمة
width * 4 - قد يكون اتجاه الأعلى/الأسفل معكوساً
- التعامل يختلف قليلاً بين
IMF2DBufferوIMFMediaBuffer
لذا تطبّع هذه الشيفرة أولاً إلى مخزن BGRA من نوع top-down ثم ترسم. توحيد هذا مسبقاً يجعل شيفرة جانب الرسم صادقة كثيراً.
flowchart TB
accTitle: لماذا التطبيع قبل الرسم
accDescr: يبين المخطّط امتصاص تزعزع مثل عدم تطابق stride مع أربعة أمثال العرض، وعكس اتجاه الأعلى/الأسفل، وفرق التعامل بين IMF2DBuffer وIMFMediaBuffer بالتطبيع أولاً إلى مخزن BGRA من نوع top-down ثم الرسم.
ir1["stride لا يتطابق"] --> nr1["التطبيع إلى BGRA من نوع top-down"]
ir2["اتجاه الأعلى/الأسفل قد ينعكس"] --> nr1
ir3["فرق التعامل بحسب نوع المخزن"] --> nr1
nr1 --> nr2["شيفرة الرسم تصير صادقة"]
الشكل 12: امتصاص تزعزع الذاكرة في موضع واحد يغني جانب الرسم عن المعرفة.
7.3 ReadSample يُنظر إليه لا في HRESULT وحده بل في flags وsample أيضاً
قد يرجع ReadSample القيمة S_OK ويكون sample == nullptr.
المثال النموذجي:
MF_SOURCE_READERF_STREAMTICKMF_SOURCE_READERF_ENDOFSTREAM- أحداث تدفق أخرى
لذا في الحلقة يلزم النظر إلى الثلاثة معاً: HRESULT وflags وinputSample.
إغفال STREAMTICK وENDOFSTREAM خصوصاً يسهّل انهيار معالجة الخط الزمني في المرحلة التالية.
flowchart TB
accTitle: ثلاث نقاط تُنظر في ReadSample
accDescr: يبين المخطّط أن ReadSample قد يرجع S_OK ويكون sample بمؤشّر nullptr، وأن أحداث التدفق مثل STREAMTICK وENDOFSTREAM نموذجية، لذا في الحلقة يلزم تأكيد HRESULT والـ flags وsample معاً.
rs1["يرجع ReadSample"] --> rs2["أكّد HRESULT"]
rs2 --> rs3["أكّد flags"]
rs3 --> rs4["أكّد وجود sample أم لا"]
rs4 -.-> rs5["حتى مع S_OK قد يكون nullptr"]
الشكل 13: لا تحكم بقيمة رجوع واحدة؛ عامل الإطار بمجموعة من ثلاث نقاط.
7.4 timestamp وduration أسلم أن يُورَّثا من الإدخال
طابع الزمن بوحدة 100ns.
كما يلزم أخذ duration على حدة من IMFSample.
أمتن من الجمع المفروض في كل مرّة بافتراض fps ثابت أن تورّث timestamp / duration لعيّنة الإدخال قدر الإمكان.
في هذه العيّنة أيضاً الاحتياطي إلى قيمة افتراضية محسوبة من fps فقط عندما يتعذّر أخذ duration.
flowchart TB
accTitle: التعامل مع timestamp وduration
accDescr: يبين المخطّط توريث timestamp وduration لعيّنة الإدخال قدر الإمكان بدل الجمع المفروض بافتراض fps ثابت، والاحتياطي إلى قيمة افتراضية محسوبة من fps فقط عندما يتعذّر أخذ duration.
ts1["استخرج من عيّنة الإدخال"] --> ts2{"هل أُخذ duration؟"}
ts2 -->|"أُخذ"| ts3["ورّث كما هو"]
ts2 -->|"تعذّر"| ts4["استخدم القيمة الافتراضية من fps"]
ts3 -.-> ts5["أمتن من الجمع المفروض"]
الشكل 14: الموقف الأساس ألا تصنع الزمن بنفسك بل تورّثه من الإدخال.
7.5 GDI+ خفيف الإدخال، لكن للفيديو الطويل والدقة العالية مرحلة تالية
GDI+ يناسب عيّنة مكتملة في ملف واحد كثيراً، أما في الفيديو الطويل أو معالجة 4K بكميات كبيرة فقد يكون D3D11 + Direct2D + DirectWrite أوفر.
- أمرّ الكل بـ
GDI+أولاً - ثم استبدل بـ
Direct2D / DirectWriteإن لزم - مل بتحويل الألوان إلى
Video Processor MFTأو جانب GPU
مسار مرحلي كهذا يسهّل التوسّع دون كسر التصميم.
flowchart TB
accTitle: مسار مرحلي لواجهة الرسم
accDescr: يبين المخطّط مساراً مرحلياً لا يكسر التصميم: أمرار الكل بـ GDI+ أولاً، ثم الاستبدال بـ Direct2D وDirectWrite إن لزم، والميل بتحويل الألوان إلى Video Processor MFT أو جانب GPU.
gd1["أمرّ الكل بـ GDI+ أولاً"] --> gd2["استبدل بـ Direct2D إن لزم"]
gd2 --> gd3["مل بتحويل الألوان إلى MFT أو GPU"]
gd1 -.-> gd4["المعالجة الكثيرة للفيديو الطويل و4K مرحلة تالية"]
الشكل 15: ابدأ بخفّة الإدخال، واستبدل بالترتيب من المواضع التي تلزم فيها الأداء.
7.6 هذه العيّنة مضيّقة على الفيديو فقط
حشر الصوت أيضاً في المقالة نفسها يشتّت محور الحديث بسهولة. لذا تركّز هذه العيّنة على حرق صورة ونص في إطار الفيديو، فالخرج MP4 فيديو فقط.
عملياً أسهل التوسّع في المرحلة التالية إلى تكوين:
- الفيديو فقط
Source Reader -> الدمج -> Sink Writer - الصوت remux مضغوطاً كما هو
8. إن لم تكن «بيانات الفيديو المعطاة» ملفاً بل سلسلة بايتات MP4 في الذاكرة
تستخدم شيفرة هذه المرّة MFCreateSourceReaderFromURL فالإدخال مسار ملف.
غير أن التفكير لا يتغيّر إن كان المتطلّب «الشيء نفسه على سلسلة بايتات mp4 وردت من API». ما يتغيّر هو المدخل فقط.
- جهّز
IStreamأو تدفّقاً خاصاً - مرّره إلى
Source ReaderكـIMFByteStream - وما بعده كذلك
RGB32 -> الرسم -> NV12 -> Sink Writer
أي الجوهر ليس كيف تُحمل بيانات الفيديو بل كيف ترسم في كل إطار بعد فك التشفير.
flowchart TB
accTitle: حتى إن كان الإدخال سلسلة بايتات غيّر المدخل فقط
accDescr: يبين المخطّط أنه إن كان الإدخال مسار ملف فـ MFCreateSourceReaderFromURL، وإن كان سلسلة بايتات MP4 في الذاكرة فجهّز IMFByteStream ومرّره إلى Source Reader، وأن التدفّق بعد ذلك من RGB32 إلى الرسم وNV12 وSink Writer هو نفسه.
in1["مسار ملف"] --> sr1["Source Reader"]
in2["سلسلة بايتات في الذاكرة"] --> bs1["اجعله IMFByteStream"]
bs1 --> sr1
sr1 --> same1["التدفّق بعد ذلك هو نفسه"]
الشكل 16: حتى إن تغيّرت طريقة حمل البيانات، ما يتغيّر مرحلة المدخل وحدها.
9. إن مددت للإنتاج
9.1 أضف remux الصوت
أول جهة توسّع الأقرب إلى الواقع إبقاء الصوت كما هو. تكوين يعيد ترميز الفيديو فقط ويكتب الصوت مضغوطاً بالصيغة نفسها يلبّي المتطلّب دون تكبير التنفيذ كثيراً.
يدعم Sink Writer صراحة تركيبة كتابة إدخال مضغوط إلى الخرج بالصيغة نفسها لأغراض remux بلا إعادة ترميز. ما يُضاف ثلاثة مواضع.
- استقبل تدفق الصوت مضغوطاً كما هو. فعّل بـ
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE)، ومرّر النوع المأخوذ بـGetNativeMediaTypeكما هو إلىSetCurrentMediaType. ممارسة جانب Source Reader: إن لم ترد فك التشفير فحدّد النوع الأصلي - اضبط ذلك النوع في Sink Writer إدخالًا وخرجًا. مرّر نوع الوسائط نفسه إلى
writer->AddStream(audioType.Get(), &audioStreamIndex)وwriter->SetInputMediaType(audioStreamIndex, audioType.Get(), nullptr) - اجعل مرجع timestamp مشتركاً مع الفيديو. اطرح
firstTimestampالذي تستخدمه شيفرة 6.2 للفيديو بالمثل من عيّنة الصوت أيضاً. جعله مرجعين منفصلين يزيح الصوت عن الصورة
غيّر أيضاً استدعاء ReadSample من الشكل الحالي الذي يحدّد الفيديو فقط إلى استخدام MF_SOURCE_READER_ANY_STREAM والتوزيع بمعرّف التدفق في قيمة الرجوع.
لاحظ أن Sink Writer لا يقوم بإعادة عيّنة الصوت ولا بتغيير حجم الفيديو ولا بتحويل معدّل الإطارات ما لم يوفّر المرمّز ذلك. إن لم يتقبّل مصب MP4 صيغة صوت الإدخال لزم إعادة الترميز لا remux.
flowchart TB
accTitle: ثلاثة مواضع تُضاف في remux الصوت
accDescr: يبين المخطّط أنه لإبقاء الصوت كما هو تُضاف ثلاثة مواضع: استقبال تدفق الصوت مضغوطاً كما هو، وضبط النوع نفسه إدخالًا وخرجًا في Sink Writer، وجعل مرجع timestamp مشتركاً مع الفيديو.
ar1["استقبل الصوت مضغوطاً كما هو"] --> ar2["اضبط النوع نفسه إدخالًا وخرجًا"]
ar2 --> ar3["اجعل مرجع timestamp مشتركاً مع الفيديو"]
ar3 -.-> ar4["فصل المرجع يزيح الصوت"]
الشكل 17: إضافة remux ثلاثة مواضع فقط، لكن لا تنسَ مواءمة مرجع الزمن.
9.2 أدرج Video Processor MFT
عيّنة هذه المرّة تقدّم الاكتمال في ملف واحد فتحوّل BGRA -> NV12 بنفسها، لكن في الإنتاج تكوين إدراج Video Processor MFT خيار قوي جداً أيضاً.
باستخدام Video Processor MFT يسهل معالجة دفعة:
- تحويل فضاء الألوان
- تغيير الحجم
- إزالة التداخل
- تحويل معدّل الإطارات
9.3 استبدل GDI+ بـ Direct2D / DirectWrite
طبقة مثل صورة الشعار والترجمة وطابع الزمن كثيراً ما يكفي فيها GDI+، لكن لضغط الأداء Direct2D / DirectWrite أوفر.
خصوصاً إن وُجدت شروط مثل:
- دقة عالية
- طول كبير
- عدد كبير من الملفات
- الرغبة لاحقاً في الميل إلى مسار GPU
دخل تكوين يستخدم D3D11 / DXGI surface في الأفق.
9.4 انظر في custom MFT عندما يصير «مؤثّر فيديو تريد إعادة استخدامه»
في Media Foundation يمكن تنفيذ المؤثّر كـ IMFTransform.
لذا إن أردت إعادة استخدام معالجة الطبقة نفسها في تطبيقات متعدّدة أو خط أنابيب، فـ custom MFT خيار نظيف.
غير أنه كأوّل نسخة:
- يلزم استيفاء عقد
IMFTransform - تزيد إدارة أنواع وسائط الإدخال والإخراج
- ترتفع صعوبة التسجيل والتنقيح
لذا عملياً أسهل التقدّم في الغالب بـ التشغيل الصحيح أولاً بـ Source Reader + الدمج + Sink Writer، والاستخراج كـ MFT عندما يلزم.
flowchart TB
accTitle: توقيت النظر في custom MFT
accDescr: يبين المخطّط تدفّق حكم التشغيل الصحيح أولاً بالتكوين الحالي Source Reader والدمج وSink Writer، ثم الاستخراج كـ custom MFT عندما تريد إعادة استخدام معالجة الطبقة نفسها في تطبيقات متعدّدة أو خط أنابيب.
mf1["شغّل صحيحاً بالتكوين الحالي أولاً"] --> mf2{"هل صرت تريد إعادة الاستخدام؟"}
mf2 -->|"تريد الاستخدام في تطبيقات متعدّدة"| mf3["استخرج إلى custom MFT"]
mf2 -->|"نسخة واحدة تكفي"| mf4["أبقِ التكوين الحالي"]
mf3 -.-> mf5["ترتفع صعوبة التنفيذ والتسجيل والتنقيح"]
الشكل 18: التجزيء نظيف، لكن ليس متأخراً أن تنتظر حتى يلزم.
9.5 اجعل السلسلة وسيطاً وتمكّن من رسم العربية أو اليابانية
سلسلة العيّنة ثابتة HelloWorld في kOverlayText. في غرض حرق رقم الجهاز أو اسم المشغّل تريد جعل هذا وسيطاً أولاً. لرسم اليابانية (أو أي كتابة تحتاج عائلة خطوط أوسع) يلزم تغيير الخط أيضاً.
التغيير أربعة مواضع.
1. أضف اسم الخط المستخدم في نطاق بلا اسم. أبقِ kOverlayText القائم قيمة افتراضية.
const wchar_t* kOverlayText = L"HelloWorld"; // 既存。引数省略時の既定値として使う
const wchar_t* kFontFamilyName = L"Yu Gothic UI"; // 日本語が出る書体
const wchar_t* kFallbackFontFamilyName = L"Segoe UI"; // 上が無い環境向け
2. اجعل تمرير سلسلة الرسم إلى DrawOverlay ممكناً.
void DrawOverlay(
std::vector<BYTE>& bgra,
UINT32 width,
UINT32 height,
Gdiplus::Image& overlayImage,
const std::wstring& overlayText) // 追加
3. داخل DrawOverlay استبدل توليد الخط ومرجع السلسلة. استبدل سطر Gdiplus::Font font(L"Segoe UI", ...) الأصلي بالتالي.
// 指定した書体が入っていなければ、既定の書体へ落とす
Gdiplus::FontFamily preferred(kFontFamilyName);
Gdiplus::FontFamily fallback(kFallbackFontFamilyName);
const Gdiplus::FontFamily& family = preferred.IsAvailable() ? preferred : fallback;
if (!family.IsAvailable())
{
throw std::runtime_error("Neither the preferred nor the fallback font family is installed.");
}
Gdiplus::Font font(&family, fontPx, Gdiplus::FontStyleBold, Gdiplus::UnitPixel);
ThrowIfGdiplusError(font.GetLastStatus(), "Create font");
ثم استبدل كل kOverlayText الممرَّر إلى MeasureString واستدعاءَي DrawString بـ overlayText.c_str(). إن لم تصلح المواضع الثلاثة بقي الظل بالحرف القديم.
4. استقبل الوسيط في wmain ومرّره إلى DrawOverlay.
if (argc < 4 || argc > 5)
{
std::wcerr
<< L"Usage: OverlayMp4.exe <input.mp4> <overlayImage.png> <output.mp4> [text]"
<< std::endl;
return 1;
}
const std::wstring inputPath = argv[1];
const std::wstring imagePath = argv[2];
const std::wstring outputPath = argv[3];
const std::wstring overlayText = (argc == 5) ? std::wstring(argv[4]) : std::wstring(kOverlayText);
ثم اجعل الاستدعاء داخل الحلقة كالتالي.
DrawOverlay(bgra, videoInfo.width, videoInfo.height, overlayImage, overlayText);
لرسم اليابانية نقطتا انتباه.
- إن كتبت يابانية حرفياً في
.cppفاحفظ المصدر UTF-8 with BOM أو ابنِ مع/utf-8في MSVC. إسقاط هذا يشوّه الحروف. التمرير من وسيط سطر الأوامر يستقبلهwmainبـ UTF-16 فلا تقع هذه المشكلة - الخط ليس مضموناً في البيئة. جهّز احتياطياً حتماً كما في الشيفرة أعلاه. السقوط الصامت إلى خط آخر في بيئة بلا الخط يصعّب تتبع سبب انزياح التخطيط
flowchart TB
accTitle: مواضع التغيير لرسم اليابانية
accDescr: يبين المخطّط أنه لجعل السلسلة وسيطاً ورسم اليابانية تُغيَّر أربعة مواضع: إضافة ثابت اسم الخط، وإضافة وسيط إلى DrawOverlay، والاستبدال بتوليد خط مع احتياطي، واستقبال الوسيط في wmain وتمريره، مع الانتباه إلى تشويه الحروف ونقص الخط.
jp1["أضف ثابت اسم الخط"] --> jp2["أضف وسيطاً إلى DrawOverlay"]
jp2 --> jp3["ولّد الخط مع احتياطي"]
jp3 --> jp4["استقبل الوسيط في wmain ومرّره"]
jp3 -.-> jp5["أسقط الخط الناقص إلى الافتراضي"]
الشكل 19: التغيير أربعة مواضع، وتجهيز الخط وترميز الحروف يسهّلان التعثّر.
10. الخلاصة
عند حرق صورة أو نص في كل إطارات فيديو MP4 بـ Media Foundation يسهل الأفق إن فكّكت إلى هذه الأربعة.
- الاستخراج:
IMFSourceReader - الرسم:
GDI+أوDirect2D / DirectWrite - التصحيح إلى شكل يتقبّله المرمّز:
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 وفضاء الألوان وإدارة السطح دفعة واحدة. أمرار المراحل مفصولة أولاً ثم تقوية المواضع اللازمة فقط أسهل كثيراً في التصميم والتنقيح.
flowchart TB
accTitle: ترتيب التمديد للإنتاج
accDescr: يبين المخطّط أن التمديد يصعب كسره بهذا الترتيب: إضافة remux الصوت، واستبدال GDI+ بـ Direct2D وDirectWrite، والميل بتحويل NV12 إلى Video Processor MFT أو جانب GPU، والتقدّم إلى أساس D3D11 surface للفيديو الطويل والدقة العالية، والاستخراج إلى custom MFT إن لزمت قابلية إعادة الاستخدام.
ex1["أضف remux الصوت"] --> ex2["استبدل الرسم بـ Direct2D"]
ex2 --> ex3["مل بالتحويل إلى MFT أو GPU"]
ex3 --> ex4["تقدّم إلى أساس D3D11 surface"]
ex4 --> ex5["استخرج إلى custom MFT إن لزم"]
الشكل 20: تجنّب الحشر الكلّي؛ قوّ مرحلة مرحلة بهذا الترتيب.
11. مقالات ذات صلة
- ما هو Media Foundation - لماذا يبدأ في الإحساس بأنّه COM وواجهات Windows الإعلاميّة في آنٍ واحد
- كيفية استخراج صورة ثابتة من MP4 باستخدام Media Foundation
12. روابط مرجعية
- طقم عيّنة هذه المقالة (ملف
.cppمكتمل وبنية بناء CMake) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/media-foundation-overlay-image-text-on-mp4-frames - 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
نرتّب هنا تحويل إطارات YUV إلى RGB في Media Foundation عبر مسارَي التحويل التلقائيّ في Source Reader والتحويل اليدويّ لـ NV12 و YUY2، مع ...
استخراج صورة ثابتة من MP4 عند زمن محدّد بـ Media Foundation
نلخّص خطوات التنفيذ لأخذ إطار قريب من زمن محدّد في MP4 عبر Source Reader، وترتيب stride و alpha في RGB32، ثم الحفظ PNG.
مدخل إلى Media Foundation - فهم الواجهة من منظور COM
نرتّب ما هو Media Foundation مع المصطلحات الأساسيّة لواجهات وسائط Windows مثل COM و HRESULT و IMFSourceReader و MFT، بالترتيب الذي يُضبَط...
Time Travel Debugging ── تسجيل الأخطاء التي لا تتكرّر في التطبيقات طويلة التشغيل وإرجاعها
خطأ يظهر مرّة في الشهر لا يترك في تفريغ الانهيار سوى نتيجته. سجّل التنفيذ وأرجعه بـ WinDbg Time Travel Debugging (TTD): TTD.exe والمخزن ا...
لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows
في Windows لا توجد مصفوفة وسائط؛ يصل إلى CreateProcess سلسلة واحدة ويتولّى الطرف المستقبل تقسيمها. نشرح قواعد CommandLineToArgvW وCRT و.N...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
موضوع يتّصل مباشرة بتنفيذ تطبيق Windows يمتدّ عبر Media Foundation وGDI+ وDirect2D / DirectWrite وتحويل الألوان وإخراج الفيديو.
الاستشارات التقنية ومراجعة التصميم
كما أنه مناسب لترتيب التصميم من حيث كيفية توسيع تنفيذ في ملف واحد ليصير تكويناً صالحاً للإنتاج، وأين يُفصل بين remux الصوت والانتقال إلى المعالجة على GPU.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- ما التدفّق الأساسي لحرق صورة أو نص في كل إطار من MP4 بـ Media Foundation؟
- الشكل الأساسي «فك التشفير بـ Source Reader → الدمج على إطار غير مضغوط → تحويل الألوان إن لزم → إعادة الترميز بـ Sink Writer». وضع الصورة أو النص نفسه ليس عمل Media Foundation، بل عمل واجهات رسم مثل GDI+ وDirect2D/DirectWrite وWIC. لتشغيل أوّل نسخة، تكوين Source Reader→RGB32→الرسم بـ GDI+→NV12→Sink Writer واضح، وإن قدّمت السرعة وقابلية التوسّع فمل نحو تكوين يستخدم سطح D3D11/DXGI وDirect2D/DirectWrite.
- هل يمكن تمرير إطار RGB32 كما هو إلى ترميز H.264؟
- ليس مضموناً. مرمّز H.264 من Microsoft يفترض غالباً مدخلاً من عائلة YUV مثل I420/IYUV/NV12/YUY2/YV12، لذا بعد الدمج بصيغة RGB32/ARGB32 الأسهل للرسم يلزم غالباً مرحلة تحويل. إمّا إدراج Video Processor MFT لتحويل RGB32→NV12، أو إدخال تحويل RGB→NV12 خاص بك. وNV12 بصيغة 4:2:0 لذا يلزم أن يكون عرض الإطار وارتفاعه زوجيين.
- أيهما ينبغي استخدامه لرسم الطبقة: GDI+ أم Direct2D؟
- في التنفيذ الأوّل يناسب GDI+ عيّنة مكتملة في ملف واحد لأنه يقرأ الصور ويرسم النص بجهد إعداد قليل. أما في الفيديو الطويل و4K والمعالجة الكثيرة فقد يكون D3D11+Direct2D+DirectWrite أوفر أداءً. مسار مرحلي لا يكسر التصميم: أمرّ الكل بـ GDI+ أولاً، ثم استبدل بـ Direct2D/DirectWrite عند ضغط السرعة، ومل بتحويل الألوان إلى Video Processor MFT أو جانب GPU.
- ما الذي ينبغي الانتباه إليه عند استخدام ReadSample في IMFSourceReader؟
- قد يرجع ReadSample S_OK ويكون sample بمؤشّر nullptr. المثال النموذجي أحداث تدفق مثل MF_SOURCE_READERF_STREAMTICK وMF_SOURCE_READERF_ENDOFSTREAM. لذا في الحلقة يلزم تأكيد الثلاثة معاً: HRESULT والـ flags وsample. وطابع الزمن بوحدة 100ns، وduration أمتن أن تُورَّث قدر الإمكان من timestamp وduration لعيّنة الإدخال بدل الجمع المفروض بافتراض fps ثابت.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.