كيف تحرق صورة ونصّاً في إطارات 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 الصوت في الملف نفسه، لكن موضوع المقالة «حرق صورة ونص في كل إطار»، لذا نضيّق هناك أولاً.

كيف تضيّق هذه العيّنةيبين المخطّط أن عيّنة المقالة تقدّم اللصق والتشغيل كما هما أولاً فتعيد ترميز الفيديو فقط، وأن remux الصوت يُؤجَّل إلى توسّع بعد أمرار الموضوع وهو الحرق.قدّم اللصق والتشغيل أولاًأعد ترميز الفيديو فقطضيّق على الحرق في كل إطارأجّل 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. لماذا هذه المسألة معقّدة قليلاً

«إدخال نص في الفيديو» يخلط في الواقع أربعة أحاديث.

  1. حديث الحاوية والمرمّز mp4 حاوية وليست الإطار نفسه. المحتوى غالباً بيانات مضغوطة بـ H.264 أو H.265.

  2. حديث فك التشفير / الترميز على البيانات المضغوطة كما هي لا تضع نصّاً أو PNG بواجهة رسم ثنائية الأبعاد عادية. يلزم أولاً العودة إلى إطار غير مضغوط.

  3. حديث الرسم النص، والشعار، ودمج شفافية PNG، ورسم النص مع تنعيم الحواف ليست دور Media Foundation نفسه. هذا عمل GDI+ أو Direct2D / DirectWrite / WIC.

  4. حديث فضاء الألوان وصيغة البكسل الصيغة الأسهل للرسم والصيغة التي يفضّلها المرمّز لا تتطابقان. هنا موضع تعثّر هادئ.

بكلمة فجّة، الأسهل ترتيباً التفكير ليس «إدخال نص بـ Media Foundation» بل «تدوير الإطارات بـ Media Foundation، والوضع بواجهة رسم، وإدخال تحويل الألوان اللازم ثم الترميز».

أربعة أحاديث مختلطةيبين المخطّط أن متطلّب إدخال نص في الفيديو يخلط أربعة أحاديث: الحاوية والمرمّز، وفك التشفير والترميز، والرسم، وفضاء الألوان وصيغة البكسل.إدخال نص في الفيديوحديث الحاوية والمرمّزحديث فك التشفير والترميزحديث الرسمحديث فضاء الألوان وصيغة البكسل

الشكل 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 صورة المعالجة

input.mp4IMFSourceReaderإطار غير مضغوطRGB32رسم الصورة + HelloWorld بـ GDI+تحويل BGRA -> NV12IMFSinkWriteroutput.mp4عيّنة صوتنسخ كما هيأو إعادة ترميز

الشكل 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.

الخطوة الأولى لأي صيغة تستقبليبين المخطّط أن تبسيط التنفيذ يعني الاستقبال بـ RGB32 أو ARGB32، وتقديم كفاءة الترميز يعني الاستقبال بـ YUV مثل NV12، لكن دمج النص أو PNG أسهل تفكيراً في عائلة RGB لذا أسهل الاستقبال بعائلة RGB في الخطوة الأولى.سهولة التنفيذكفاءة الترميزماذا تقدّم؟استقبل بـ RGB32 / ARGB32استقبل بـ YUV مثل NV12الدمج أسهل تفكيراً في عائلة RGB

الشكل 4: إن تحيّرت فقدّم سهولة الرسم وابدأ الاستقبال بعائلة RGB.

تفعيل MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING يجعل Source Reader يحوّل YUV -> RGB32 ويزيل التداخل. هذا مريح في مرحلة «أريد استخراج الإطار والتعامل معه أولاً»، لكنه يثقل بسهولة في الفيديو الطويل أو العالي الدقة، فإن لزمت السرعة في الإنتاج استحق التكوين مراجعة لاحقاً.

ربح ENABLE_VIDEO_PROCESSING وخسارتهيبين المخطّط أن تفعيل MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING يجعل Source Reader يتولّى التحويل من YUV إلى RGB32 وإزالة التداخل، لكنه يثقل بسهولة في الفيديو الطويل أو العالي الدقة، فإن لزمت السرعة في الإنتاج استحق التكوين مراجعة.فعّل العلامةأوكل التحويل من YUV إلى RGB32أوكل أيضاً إزالة التداخليثقل بسهولة في الطويل والعالي الدقة

الشكل 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 الذي يعالج تحويل فضاء الألوان وتغيير الحجم وإزالة التداخل دفعة خيار قوي أيضاً.

طريقان يربطان RGB بـ NV12يبين المخطّط أنه بعد الدمج بصيغة RGB32 أو ARGB32 الأسهل للرسم يلزم إمّا التحويل بإدراج Video Processor MFT أو إدخال تحويل خاص من RGB إلى NV12، وأن هذه العيّنة تقدّم الاكتمال في ملف واحد فتختار التحويل الخاص.اكتمل الدمج بعائلة RGBحوّل بـ Video Processor MFTحوّل إلى NV12 بنفسكالعيّنة تقدّم الاكتمال في ملف واحدتكوين قوي في الإنتاج

الشكل 6: افترض أن مرحلة التحويل لازمة حتماً، واختر فقط أين تحملها.

4.4 الخرج يُكتب بـ IMFSinkWriter

خرج الفيديو يسهل التعامل معه بـ IMFSinkWriter.

التفكير بسيط:

  • نوع تدفق الخرج … الصيغة التي تريد كتابتها في الملف مثال: MFVideoFormat_H264
  • نوع تدفق الإدخال … الصيغة التي يمرّرها التطبيق إلى Sink Writer مثال: MFVideoFormat_NV12

تُضبطان منفصلتين.

أي من منظور Sink Writer:

  • جانب التطبيق يمرّر إطاراً غير مضغوط بصيغة NV12
  • Sink Writer يرمّزه إلى H.264 ويكتبه في MP4

هذه العلاقة.

تقسيم نوعي إدخال Sink Writer وخرجهيبين المخطّط أن Sink Writer يُضبط له NV12 نوع تدفق إدخال يمرّره التطبيق، وH.264 نوع تدفق خرج يُكتب في الملف، منفصلين، ويتولّى Sink Writer الترميز والكتابة إلى MP4.التطبيق يمرّر إطار NV12Sink Writerالترميز إلى H.264الكتابة إلى MP4اضبط نوع الإدخال ونوع الخرج منفصلين

الشكل 7: ممارسة Sink Writer ضبط الصيغة الممرَّرة والصيغة المكتوبة منفصلتين.

4.5 التفكير في الصوت منفصلاً أولاً يسهّل الترتيب

كثير جداً أن تريد إدخال شعار أو نص في الفيديو دون تغيير الصوت نفسه.

عملياً يسهل استخدام تكوين:

  • تدفق الفيديو فقط Source Reader -> الدمج -> Sink Writer
  • تدفق الصوت remux مضغوطاً كما هو

غير أن عيّنة هذه المرّة تركّز على حرق صورة ونص في الإطار، فالخرج MP4 فيديو فقط. نسخة تُبقي الصوت أسهل تتبعاً إن أُضيفت في مرحلة التوسّع بعد ذلك.

فكّر في الفيديو والصوت منفصلينيبين المخطّط أن التكوين العملي يمرّر تدفق الفيديو وحده من Source Reader عبر الدمج إلى Sink Writer، ويعمل remux لتدفق الصوت مضغوطاً كما هو، وأن هذه العيّنة تضيّق التركيز فالخرج فيديو فقط.تدفق الفيديوادمج ومرّر إلى Sink Writerتدفق الصوتremux مضغوطاً كما هوالعيّنة لا تعالجه

الشكل 8: إن كان ما تريد تغييره الفيديو فقط فانقل الصوت في وعائه دون لمسه.

5. افتراضات هذه العيّنة وطريقة استخدامها

افتراضات هذه الشيفرة كالتالي.

  • Windows 10 / 11
  • تطبيق وحدة تحكّم C++ في Visual Studio 2022
  • بناء x64
  • ملف .cpp هذا لا يستخدم ترويسة مترجمة مسبقاً
  • عرض فيديو الإدخال وارتفاعه زوجيان
  • الإدخال ملف فيديو MP4 عادي
  • الخرج MP4 فيديو فقط
  • الصورة صيغة يقرأها GDI+ مثل PNG / JPEG / BMP / GIF

NV12 بصيغة 4:2:0 لذا يلزم أن يكون العرض والارتفاع زوجيين. لذلك توقّف هذه العيّنة بخطأ صريح إن لم يُستوف الشرط.

افتراض أن العرض والارتفاع زوجيانيبين المخطّط أن NV12 عيّنة فرعية 4:2:0 فيلزم أن يكون عرض فيديو الإدخال وارتفاعه زوجيين، وأن هذه العيّنة توقّف بخطأ صريح إن لم يُستوف الشرط.زوجييتضمّن فردياًNV12 بصيغة 4:2:0يلزم عرض وارتفاع زوجيانهل الإدخال زوجي؟تابع المعالجةتوقّف بخطأ صريح

الشكل 9: بدل الانهيار صامتاً أوقف الإدخال الذي لا يستوفي الافتراض عند المدخل.

5.1 طريقة الاستخدام

  1. اصنع Console App في Visual Studio
  2. الصق هذا .cpp كلّه
  3. اجعل الترويسة المترجمة مسبقاً لـ .cpp ذلك «لا تُستخدم»
  4. ابنِ بـ x64
  5. شغّل كالتالي
OverlayMp4.exe input.mp4 overlay.png output.mp4
  • input.mp4 الفيديو الأصلي
  • overlay.png الصورة المراد وضعها
  • output.mp4 جهة الخرج

السلسلة ثابتة HelloWorld في kOverlayText عند رأس الشيفرة. الموضع والحجم يُغيَّران بلمس الثوابت داخل الشيفرة. التعديل ليصير التمرير وسيط سطر أوامر موضوع في 9.5.

5.2 التحقق من أن التشغيل صحيح

«انتهى بلا خطأ» شيء و«حُرق صحيحاً» شيء آخر. النظر إلى الأربعة التالية بالترتيب يجد معظم الإخفاق.

  1. انظر إلى عدد الإطارات عند الإنهاء. تخرج هذه العيّنة عند انتهاء المعالجة عدد الإطارات المكتوبة بعد Done. frames=. إن انزاح كثيراً عن إجمالي إطارات فيديو الإدخال فقد أسقطت الحلقة شيئاً في موضع ما من ReadSample
  2. قارن المعلومات الأساسية لملف الخرج بالإدخال. انقر باليمين على MP4 الخرج في المستكشف، وافتح الخصائص > التفاصيل، فيظهر الطول وعرض الإطار وارتفاعه ومعدّل الإطارات. إن لم يتطابق الطول مع الإدخال فاشك في التعامل مع timestamp (7.4)
  3. انظر بالعين إلى ثلاثة مواضع: الرأس والوسط والنهاية. الاطمئنان بإطار الرأس وحده يفوّت عيباً تختفي فيه الطبقة في المنتصف. الأضمن قص صورة ثابتة من الإدخال والخرج عند اللحظة نفسها وصفّهما، والإجراءات في «كيفية استخراج صورة ثابتة من MP4 باستخدام Media Foundation»
  4. انظر إن كان اللون غريباً. إن صار لون بشرة الإنسان أو السماء غير طبيعي فقد لا يطابق اختيار معاملات BgraToNv12 (BT.601 وBT.709) الإدخال

عند التجربة الأولى يُوصى بـ MP4 قصير من ثوانٍ وPNG واضحة المحيط. محاولة أمرار أوّل نسخة بفيديو طويل تطيل فصل المشكلة.

إجراءات التحقق من أن التشغيل صحيحيبين المخطّط أن الانتهاء بلا خطأ والحرق الصحيح شيئان مختلفان، لذا يُنظر بالترتيب إلى مطابقة عدد الإطارات، ومقارنة المعلومات الأساسية لملف الخرج، والنظر بالعين إلى الرأس والوسط والنهاية، وتأكيد شذوذ اللون.طابق عدد الإطارات مع الإدخالقارن الطول والحجم في الخصائصانظر بالعين إلى الرأس والوسط والنهايةأكّد عدم طبيعية اللونإن شذ فاشك في اختيار المعاملات

الشكل 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».

الدوال الثلاث المركزية التي ينبغي قراءتهايبين المخطّط أن المركز الذي ينبغي قراءته فعلاً في الشيفرة الطويلة ثلاثة CopySampleToTopDownBgra وDrawOverlay وBgraToNv12 وحلقة wmain، وتقابل التطبيع إلى إطار غير مضغوط والرسم والتحويل إلى NV12.حلقة wmain تدور إطاراً إطاراًCopySampleToTopDownBgraDrawOverlayBgraToNv12التطبيع إلى إطار غير مضغوطرسم الصورة والنصالتهيئة للترميز

الشكل 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, &currentType), "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, &currentLength), "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,
                    &timestamp,
                    &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 ثم ترسم. توحيد هذا مسبقاً يجعل شيفرة جانب الرسم صادقة كثيراً.

لماذا التطبيع قبل الرسميبين المخطّط امتصاص تزعزع مثل عدم تطابق stride مع أربعة أمثال العرض، وعكس اتجاه الأعلى/الأسفل، وفرق التعامل بين IMF2DBuffer وIMFMediaBuffer بالتطبيع أولاً إلى مخزن BGRA من نوع top-down ثم الرسم.stride لا يتطابقالتطبيع إلى BGRA من نوع top-downاتجاه الأعلى/الأسفل قد ينعكسفرق التعامل بحسب نوع المخزنشيفرة الرسم تصير صادقة

الشكل 12: امتصاص تزعزع الذاكرة في موضع واحد يغني جانب الرسم عن المعرفة.

7.3 ReadSample يُنظر إليه لا في HRESULT وحده بل في flags وsample أيضاً

قد يرجع ReadSample القيمة S_OK ويكون sample == nullptr. المثال النموذجي:

  • MF_SOURCE_READERF_STREAMTICK
  • MF_SOURCE_READERF_ENDOFSTREAM
  • أحداث تدفق أخرى

لذا في الحلقة يلزم النظر إلى الثلاثة معاً: HRESULT وflags وinputSample. إغفال STREAMTICK وENDOFSTREAM خصوصاً يسهّل انهيار معالجة الخط الزمني في المرحلة التالية.

ثلاث نقاط تُنظر في ReadSampleيبين المخطّط أن ReadSample قد يرجع S_OK ويكون sample بمؤشّر nullptr، وأن أحداث التدفق مثل STREAMTICK وENDOFSTREAM نموذجية، لذا في الحلقة يلزم تأكيد HRESULT والـ flags وsample معاً.يرجع ReadSampleأكّد HRESULTأكّد flagsأكّد وجود sample أم لاحتى مع S_OK قد يكون nullptr

الشكل 13: لا تحكم بقيمة رجوع واحدة؛ عامل الإطار بمجموعة من ثلاث نقاط.

7.4 timestamp وduration أسلم أن يُورَّثا من الإدخال

طابع الزمن بوحدة 100ns. كما يلزم أخذ duration على حدة من IMFSample.

أمتن من الجمع المفروض في كل مرّة بافتراض fps ثابت أن تورّث timestamp / duration لعيّنة الإدخال قدر الإمكان. في هذه العيّنة أيضاً الاحتياطي إلى قيمة افتراضية محسوبة من fps فقط عندما يتعذّر أخذ duration.

التعامل مع timestamp وdurationيبين المخطّط توريث timestamp وduration لعيّنة الإدخال قدر الإمكان بدل الجمع المفروض بافتراض fps ثابت، والاحتياطي إلى قيمة افتراضية محسوبة من fps فقط عندما يتعذّر أخذ duration.أُخذتعذّراستخرج من عيّنة الإدخالهل أُخذ duration؟ورّث كما هواستخدم القيمة الافتراضية من fpsأمتن من الجمع المفروض

الشكل 14: الموقف الأساس ألا تصنع الزمن بنفسك بل تورّثه من الإدخال.

7.5 GDI+ خفيف الإدخال، لكن للفيديو الطويل والدقة العالية مرحلة تالية

GDI+ يناسب عيّنة مكتملة في ملف واحد كثيراً، أما في الفيديو الطويل أو معالجة 4K بكميات كبيرة فقد يكون D3D11 + Direct2D + DirectWrite أوفر.

  • أمرّ الكل بـ GDI+ أولاً
  • ثم استبدل بـ Direct2D / DirectWrite إن لزم
  • مل بتحويل الألوان إلى Video Processor MFT أو جانب GPU

مسار مرحلي كهذا يسهّل التوسّع دون كسر التصميم.

مسار مرحلي لواجهة الرسميبين المخطّط مساراً مرحلياً لا يكسر التصميم: أمرار الكل بـ GDI+ أولاً، ثم الاستبدال بـ Direct2D وDirectWrite إن لزم، والميل بتحويل الألوان إلى Video Processor MFT أو جانب GPU.أمرّ الكل بـ GDI+ أولاًاستبدل بـ Direct2D إن لزممل بتحويل الألوان إلى MFT أو GPUالمعالجة الكثيرة للفيديو الطويل و4K مرحلة تالية

الشكل 15: ابدأ بخفّة الإدخال، واستبدل بالترتيب من المواضع التي تلزم فيها الأداء.

7.6 هذه العيّنة مضيّقة على الفيديو فقط

حشر الصوت أيضاً في المقالة نفسها يشتّت محور الحديث بسهولة. لذا تركّز هذه العيّنة على حرق صورة ونص في إطار الفيديو، فالخرج MP4 فيديو فقط.

عملياً أسهل التوسّع في المرحلة التالية إلى تكوين:

  • الفيديو فقط Source Reader -> الدمج -> Sink Writer
  • الصوت remux مضغوطاً كما هو

8. إن لم تكن «بيانات الفيديو المعطاة» ملفاً بل سلسلة بايتات MP4 في الذاكرة

تستخدم شيفرة هذه المرّة MFCreateSourceReaderFromURL فالإدخال مسار ملف.

غير أن التفكير لا يتغيّر إن كان المتطلّب «الشيء نفسه على سلسلة بايتات mp4 وردت من API». ما يتغيّر هو المدخل فقط.

  • جهّز IStream أو تدفّقاً خاصاً
  • مرّره إلى Source Reader كـ IMFByteStream
  • وما بعده كذلك RGB32 -> الرسم -> NV12 -> Sink Writer

أي الجوهر ليس كيف تُحمل بيانات الفيديو بل كيف ترسم في كل إطار بعد فك التشفير.

حتى إن كان الإدخال سلسلة بايتات غيّر المدخل فقطيبين المخطّط أنه إن كان الإدخال مسار ملف فـ MFCreateSourceReaderFromURL، وإن كان سلسلة بايتات MP4 في الذاكرة فجهّز IMFByteStream ومرّره إلى Source Reader، وأن التدفّق بعد ذلك من RGB32 إلى الرسم وNV12 وSink Writer هو نفسه.مسار ملفSource Readerسلسلة بايتات في الذاكرةاجعله IMFByteStreamالتدفّق بعد ذلك هو نفسه

الشكل 16: حتى إن تغيّرت طريقة حمل البيانات، ما يتغيّر مرحلة المدخل وحدها.

9. إن مددت للإنتاج

9.1 أضف remux الصوت

أول جهة توسّع الأقرب إلى الواقع إبقاء الصوت كما هو. تكوين يعيد ترميز الفيديو فقط ويكتب الصوت مضغوطاً بالصيغة نفسها يلبّي المتطلّب دون تكبير التنفيذ كثيراً.

يدعم Sink Writer صراحة تركيبة كتابة إدخال مضغوط إلى الخرج بالصيغة نفسها لأغراض remux بلا إعادة ترميز. ما يُضاف ثلاثة مواضع.

  1. استقبل تدفق الصوت مضغوطاً كما هو. فعّل بـ reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE)، ومرّر النوع المأخوذ بـ GetNativeMediaType كما هو إلى SetCurrentMediaType. ممارسة جانب Source Reader: إن لم ترد فك التشفير فحدّد النوع الأصلي
  2. اضبط ذلك النوع في Sink Writer إدخالًا وخرجًا. مرّر نوع الوسائط نفسه إلى writer->AddStream(audioType.Get(), &audioStreamIndex) وwriter->SetInputMediaType(audioStreamIndex, audioType.Get(), nullptr)
  3. اجعل مرجع timestamp مشتركاً مع الفيديو. اطرح firstTimestamp الذي تستخدمه شيفرة 6.2 للفيديو بالمثل من عيّنة الصوت أيضاً. جعله مرجعين منفصلين يزيح الصوت عن الصورة

غيّر أيضاً استدعاء ReadSample من الشكل الحالي الذي يحدّد الفيديو فقط إلى استخدام MF_SOURCE_READER_ANY_STREAM والتوزيع بمعرّف التدفق في قيمة الرجوع.

لاحظ أن Sink Writer لا يقوم بإعادة عيّنة الصوت ولا بتغيير حجم الفيديو ولا بتحويل معدّل الإطارات ما لم يوفّر المرمّز ذلك. إن لم يتقبّل مصب MP4 صيغة صوت الإدخال لزم إعادة الترميز لا remux.

ثلاثة مواضع تُضاف في remux الصوتيبين المخطّط أنه لإبقاء الصوت كما هو تُضاف ثلاثة مواضع: استقبال تدفق الصوت مضغوطاً كما هو، وضبط النوع نفسه إدخالًا وخرجًا في Sink Writer، وجعل مرجع timestamp مشتركاً مع الفيديو.استقبل الصوت مضغوطاً كما هواضبط النوع نفسه إدخالًا وخرجًااجعل مرجع timestamp مشتركاً مع الفيديوفصل المرجع يزيح الصوت

الشكل 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 عندما يلزم.

توقيت النظر في custom MFTيبين المخطّط تدفّق حكم التشغيل الصحيح أولاً بالتكوين الحالي Source Reader والدمج وSink Writer، ثم الاستخراج كـ custom MFT عندما تريد إعادة استخدام معالجة الطبقة نفسها في تطبيقات متعدّدة أو خط أنابيب.تريد الاستخدام في تطبيقات متعدّدةنسخة واحدة تكفيشغّل صحيحاً بالتكوين الحالي أولاًهل صرت تريد إعادة الاستخدام؟استخرج إلى custom MFTأبقِ التكوين الحاليترتفع صعوبة التنفيذ والتسجيل والتنقيح

الشكل 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 فلا تقع هذه المشكلة
  • الخط ليس مضموناً في البيئة. جهّز احتياطياً حتماً كما في الشيفرة أعلاه. السقوط الصامت إلى خط آخر في بيئة بلا الخط يصعّب تتبع سبب انزياح التخطيط
مواضع التغيير لرسم اليابانيةيبين المخطّط أنه لجعل السلسلة وسيطاً ورسم اليابانية تُغيَّر أربعة مواضع: إضافة ثابت اسم الخط، وإضافة وسيط إلى DrawOverlay، والاستبدال بتوليد خط مع احتياطي، واستقبال الوسيط في wmain وتمريره، مع الانتباه إلى تشويه الحروف ونقص الخط.أضف ثابت اسم الخطأضف وسيطاً إلى DrawOverlayولّد الخط مع احتياطياستقبل الوسيط في wmain ومرّرهأسقط الخط الناقص إلى الافتراضي

الشكل 19: التغيير أربعة مواضع، وتجهيز الخط وترميز الحروف يسهّلان التعثّر.

10. الخلاصة

عند حرق صورة أو نص في كل إطارات فيديو MP4 بـ Media Foundation يسهل الأفق إن فكّكت إلى هذه الأربعة.

  • الاستخراج: IMFSourceReader
  • الرسم: GDI+ أو Direct2D / DirectWrite
  • التصحيح إلى شكل يتقبّله المرمّز: NV12 ونحوه
  • الكتابة: IMFSinkWriter

وإن أردت «عيّنة تُلصق كلّها في .cpp واحد وتعمل كما هي» فتكوين كهذه المرّة

Source Reader -> RGB32 -> صورة + HelloWorld بـ GDI+ -> BGRA to NV12 -> Sink Writer

صادق جداً.

إن مددت للإنتاج فالترتيب التالي يصعب كسره.

  1. أضف remux الصوت
  2. استبدل GDI+ بـ Direct2D / DirectWrite
  3. مل بتحويل NV12 إلى Video Processor MFT أو جانب GPU
  4. تقدّم إلى أساس D3D11 surface للفيديو الطويل والدقة العالية
  5. استخرج إلى custom MFT إن لزمت قابلية إعادة الاستخدام

الحشر الكلّي دفعة يدفع COM وstride وفضاء الألوان وإدارة السطح دفعة واحدة. أمرار المراحل مفصولة أولاً ثم تقوية المواضع اللازمة فقط أسهل كثيراً في التصميم والتنقيح.

ترتيب التمديد للإنتاجيبين المخطّط أن التمديد يصعب كسره بهذا الترتيب: إضافة remux الصوت، واستبدال GDI+ بـ Direct2D وDirectWrite، والميل بتحويل NV12 إلى Video Processor MFT أو جانب GPU، والتقدّم إلى أساس D3D11 surface للفيديو الطويل والدقة العالية، والاستخراج إلى custom MFT إن لزمت قابلية إعادة الاستخدام.أضف remux الصوتاستبدل الرسم بـ Direct2Dمل بالتحويل إلى MFT أو GPUتقدّم إلى أساس D3D11 surfaceاستخرج إلى custom MFT إن لزم

الشكل 20: تجنّب الحشر الكلّي؛ قوّ مرحلة مرحلة بهذا الترتيب.

11. مقالات ذات صلة

12. روابط مرجعية

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

ما التدفّق الأساسي لحرق صورة أو نص في كل إطار من 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 ثابت.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة