استخراج صورة ثابتة من MP4 عند زمن محدّد بـ Media Foundation

· آخر تحديث: · · Media Foundation, C++, تطوير Windows, WIC

سجل التعديلات (3 تحديثات، آخر تحديث 3 Sep، 2026)

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

أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240833)
أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621362)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

小村 豪 (2026). استخراج صورة ثابتة من MP4 عند زمن محدّد بـ Media Foundation. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621362 https://comcomponent.com/ar/blog/2026/03/15/000-media-foundation-extract-still-image-from-mp4-at-specific-time/

DOI (أحدث نسخة)
10.5281/zenodo.21621362
DOI (هذه النسخة)
10.5281/zenodo.22279766

الحاجة إلى «إطار واحد عند 12.3 ثانية» من MP4 مطلب عادي جدّاً. توليد صور مصغّرة، وسجلات فحص، وإطار تمثيلي لتصوير مراقبة، وأثر سجلات جهاز، وغيرها.

غير أن Media Foundation هنا أقلّ وضوحاً قليلاً. يبدو أن استدعاء ReadSample مرّة بعد SetCurrentPosition يكفي، غير أن الإطار المفتاحي و timestamp و stride واتّجاه الصورة رأسيّاً والبايت الرابع في RGB32 تتداخل. إن تقدّمت فضفاضاً وقعت حوادث هادئة مزعجة: انحراف الزمن قليلاً، أو انقلاب الصورة رأسيّاً، أو PNG شفاف على نحو غريب.

حوادث التقدّم الفضفاضتنفيذ يسعى ثم يقرأ مرّة ويحفظ يتداخل فيه الإطار المفتاحي و timestamp و stride والاتّجاه الرأسي والبايت الرابع في RGB32، فتقع حوادث انحراف الزمن والانقلاب الرأسي و PNG الشفاف.seek ثم قراءة مرّة ثم حفظانحراف الزمن قليلاًانقلاب الصورة رأسيّاًPNG شفاف على نحو غريب

الشكل 1: تنفيذ يبدو للوهلة الأولى كافياً يؤدّي إلى حوادث هادئة: انحراف الزمن، والانقلاب الرأسي، و PNG الشفاف.

الصورة الكاملة لـ Media Foundation نفسها يفيد معها المقال السابق مدخل إلى Media Foundation - فهم الواجهة من منظور COM. هذه المرّة ننزل درجة ونضيّق على إطار واحد من MP4 فقط.

في هذه المقالة نرتّب، بفخاخ يسهل الوقوع فيها في العمل اليومي، استخدام IMFSourceReader لاستخراج صورة ثابتة واحدة هي الأقرب إلى زمن محدّد من MP4 وحفظها PNG. وفي النهاية نضع شيفرة ملف واحد يسهل لصقها كما هي في .cpp لمشروع تطبيق console بـ C++ في Visual Studio. بلا شيفرة مقطّعة في أثناء المقالة؛ يكفي أخذ الكتلة الأخيرة وحدها لتعمل.

الشيفرة الواردة في هذه المقالة منشورة على GitHub كمجموعة عينات كاملة (تطبيق console بـ C++ في ملف واحد).

media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)

1. الخلاصة أوّلاً

الخلاصة أوّلاً كالتالي.

  • لإطار واحد من MP4، Source Reader أوضح كمدخل هذه المرّة من Media Session
  • IMFSourceReader::SetCurrentPosition لا يضمن exact seek. عادة يميل إلى ما قبل الهدف قليلاً، خصوصاً نحو إطار مفتاحي، لذا يلزم بعده التقدّم بـ ReadSample ومقارنة ما قبل الزمن المستهدف وما بعده
  • ReadSample قد ينجح مع pSample == nullptr. لا تنظر إلى HRESULT فقط، بل إلى flags و pSample أيضاً
  • إزاحة نوع الوسائط الخارج إلى MFVideoFormat_RGB32 تسهّل الحفظ
  • غير أن البايت الرابع في RGB32 ليس بالضرورة alpha، فكتابته كما هو إلى PNG قد تعطي صورة شفافة. الأأمن ملء 0xFF قبل الحفظ لجعله غير شفاف
  • التعامل الفضفاض مع stride لكل صف ومع top-down / bottom-up ينهار الصورة، لذا رتّب العيّنة المستخرجة مرّة إلى BGRA متّصل من أعلى إلى أسفل ثم مرّرها إلى PNG

باختصار، seek -> قراءة مرّة -> حفظ فضفاض قليلاً، وما إن تصل إلى seek -> مقارنة ما قبل وما بعد مع مراقبة timestamp -> نسخ يراعي stride -> حفظ PNG حتى يثبت الأمر كثيراً.

التدفّق الفضفاض والتدفّق الذي يثبتتدفّق seek ثم قراءة مرّة ثم حفظ فضفاض، أمّا التدفّق الذي يقارن ما قبل وما بعد مع مراقبة timestamp بعد الـ seek ثم ينسخ بمراعاة stride ثم يحفظ PNG فيثبت كثيراً.القراءة مرّة ثم الحفظ فضفاضseekمقارنة ما قبل وما بعد مع مراقبة timestampنسخ يراعي strideحفظ PNGسبب الانحراف والانهيار

الشكل 2: لا تحفظ فور الـ seek؛ أدخل مقارنة ما قبل وما بعد ونسخاً يراعي stride فيثبت الأمر.

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 15، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. مقدّمات هذه المقالة

2.1. القرّاء المستهدفون والمعرفة المفترضة

  • نفترض من استدعى واجهة COM على Windows بـ C++. يكفي فهم تدفّق التهيئة بـ CoInitializeEx وتحرير مؤشّر الواجهة بـ Release
  • قيمة العودة HRESULT تُحكَم بماكروي SUCCEEDED / FAILED. عند الفشل تُقرأ القيمة الست عشرة كما هي. قيمة تبدأ بـ 0x8007 من خطأ Win32، والـ 16 بت الدنيا رمز خطأ Win32 (0x80070057 هي القيمة نفسها لـ E_INVALIDARG). أخطاء Media Foundation الخاصة التي تبدأ بـ MF_E_ معرَّفة في mferror.h
  • يمكن قراءة المقالة حتى إن كان Media Foundation جديداً عليك. الصورة الكاملة من منظور COM ملخَّصة في مدخل إلى Media Foundation - فهم الواجهة من منظور COM

2.2. بيئة التطوير

البند مقدّمة هذه المرّة
نظام التشغيل Windows 10 / Windows 11
بيئة التطوير Visual Studio 2022 (حمل عمل «تطوير سطح المكتب بـ C++»)
SDK Windows SDK المضمَّن مع Visual Studio (يشمل ترويسات ومكتبات Media Foundation و WIC)
المشروع قالب «تطبيق وحدة تحكّم» بـ C++
المنصّة x64
مكتبات إضافية لا شيء. تحديد الربط مثل mfplat.lib يُنجَز داخل الشيفرة بـ #pragma comment(lib, ...)

تنبيهات اللصق ومحيط الترويسة المترجمة مسبقاً ملخَّصة في «7. ملاحظات البناء والتشغيل».

2.3. مقدّمات الإدخال والخرج

مقدّمات هذه المرّة كالتالي.

  • الإدخال ملف MP4 محلّي
  • المطلوب صورة ثابتة واحدة
  • لا نعيد «الزمن المحدّد تماماً»، بل «الإطار الأقرب إلى الزمن المحدّد»
  • التنفيذ IMFSourceReader في وضع متزامن
  • صيغة الحفظ PNG عبر WIC
  • بلا مكتبة خارجية؛ يكتمل بواجهات Windows القياسية فقط
  • نفترض MP4 عامّاً لا تتغيّر فيه الدقّة في الأثناء

للتشغيل ومزامنة الصوت وشريط التمرير وربط واجهة المستخدم تصميم آخر، غير أنه لغرض إطار واحد هذا أوضح كثيراً.

تركيب المقدّمات هذه المرّةإدخال MP4 محلّي، وأخذ الإطار الأقرب إلى زمن محدّد بـ IMFSourceReader متزامن، والحفظ PNG عبر WIC، تكوين بلا مكتبة خارجية.MP4 محلّيIMFSourceReader متزامنالإطار الأقرب إلى الزمن المحدّدحفظ PNG عبر WICيكتمل بواجهات Windows القياسية فقط

الشكل 3: من الإدخال إلى الحفظ بتكوين Source Reader متزامن بلا مكتبة خارجية.

3. جداول للنظر أوّلاً

3.1. تدفّق المعالجة

ما نفعله الواجهة المستخدمة الدور
فتح MP4 MFCreateSourceReaderFromURL صنع مصدر وسائط من الملف
اختيار الفيديو فقط SetStreamSelection عدم قراءة الصوت
التحويل إلى RGB32 SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING الحصول على إطار غير مضغوط يسهل حفظه
الانتقال إلى الزمن المحدّد SetCurrentPosition seek بوحدة 100ns
قراءة الإطار ReadSample أخذ عيّنة مفكوكة واحدة تلو الأخرى
مقارنة ما قبل وما بعد sample timestamp حسم الإطار الواحد الأقرب إلى الزمن المحدّد
الحفظ PNG WIC الكتابة إلى ملف صورة

3.2. قاعدة الحكم هذه المرّة

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

هذه المرّة القاعدة كالتالي.

  • بعد الـ seek نتقدّم بـ ReadSample
  • نحتفظ بآخر عيّنة حيث timestamp < target
  • عندما تأتي أوّل عيّنة حيث timestamp >= target نقارن فرق العيّنة السابقة والعيّنة الحالية
  • نعتمد الأقرب إلى الهدف

بهذا يسهل أخذ الإطار الأقرب إلى الهدف لا «أوّل إطار بعد الهدف».

قاعدة حكم اختيار الإطار الأقرببعد الـ seek نتقدّم بـ ReadSample، ونحتفظ بآخر عيّنة قبل الهدف، وعندما تأتي أوّل عيّنة بعده نقارن فرق الاثنتين ونعتمد الأقرب إلى الهدف.التقدّم بـ ReadSample بعد الـ seekالاحتفاظ بآخر عيّنة قبل الهدفتأتي أوّل عيّنة بعد الهدفمقارنة فرق كلّ منهما عن الهدفاعتماد الأقرب

الشكل 4: مقارنة مرشّحين قبل الهدف وبعده ثم الاعتماد يأخذ الإطار الأقرب لا «أوّل إطار بعد الهدف».

3.3. صورة المعالجة

من رأس المعالجة إلى ذيلها تقريباً: input.mp4 -> صنع Source Reader -> طلب RGB32 -> seek -> تكرار ReadSample -> مقارنة ما قبل الهدف وما بعده -> إعادة الحزم إلى BGRA من أعلى إلى أسفل -> حفظ PNG عبر WIC.

المظهر بسيط، غير أن دقّة الـ seek و null في العيّنة و stride والتعامل مع البايت الرابع لكلّ منها فخّ صغير. إن لم تطأ هذه الأربعة استقرّ التنفيذ نفسه بوضوح.

أربعة فخاخ كامنة في المعالجةمظهر المعالجة بسيط، غير أن دقّة الـ seek و null في العيّنة و stride والبايت الرابع في RGB32 أربعة فخاخ صغيرة، وإن لم تُوطأ استقرّ التنفيذ بوضوح.معالجة بسيطة المظهردقّة الـ seeknull في العيّنةstride والاتّجاهالتعامل مع البايت الرابعإن لم تُوطأ استقرّ بوضوح

الشكل 5: في تدفّق يبدو بسيطاً أربعة فخاخ في مواضع، والوعي بها وحدها يجعل التنفيذ يستقرّ بوضوح.

4. فخاخ نضبطها أوّلاً

نلخّص المصطلحات التي تظهر من هذا الفصل باختصار أوّلاً.

المصطلح المعنى
إطار مفتاحي (key frame) إطار يُفكّ وحده دون الرجوع إلى إطارات سابقة أو لاحقة. في H.264 ينطبق مثلاً على صورة IDR
GOP (Group of Pictures) مجموعة إطارات من إطار مفتاحي حتى ما قبل الإطار المفتاحي التالي. كلّما طال GOP اتّسع فاصل الإطار المفتاحي، فيسهل ابتعاد موضع ما بعد الـ seek عن الزمن المحدّد
stride عدد البايتات التي يشغلها صف واحد في مخزن الصورة. لا يتطابق بالضرورة مع العرض × بايتات البكسل الواحد، فقد يدخل حشو في نهاية الصف
top-down / bottom-up هل بداية مخزن الصورة صف الحافة العليا أم الحافة السفلى. bottom-up يُعبَّر عنه بقيمة stride سالبة
MF_SOURCE_READERF_STREAMTICK أحد أعلام عودة ReadSample، ويدلّ على فجوة في الدفق (انقطاع بيانات). الاستدعاء الذي يرتفع فيه هذا العلم لا يعطي إطاراً، فأعد القراءة

4.1. SetCurrentPosition ليس exact seek

كما في IMFSourceReader::SetCurrentPosition على Microsoft Learn، هذا لا يضمن exact seeking. في الفيديو يميل عادة إلى ما قبل الموضع المحدّد قليلاً، خصوصاً نحو إطار مفتاحي. ثم المقدّمة التقدّم بـ ReadSample حتى الموضع المستهدف.

لذلك تنفيذ كهذا خطر جدّاً.

  • SetCurrentPosition(target)
  • ReadSample مرّة
  • حفظ ذلك الإطار

في فيديو GOP طويل ينحرف بهذا. إن كان فاصل الإطار المفتاحي ثانيتين، فقد تحفظ إطاراً أقرب إلى ثانيتين قبل الزمن المحدّد في الحدّ الأقصى.

آلية انحراف القراءة مرّة فور الـ seekSetCurrentPosition يهبط قبل الموضع المحدّد قليلاً خصوصاً نحو إطار مفتاحي، فإن قرأت مرّة فور ذلك وحفظت فقد تحفظ في فيديو GOP طويل إطاراً أسبق بكثير من الزمن المحدّد.SetCurrentPosition(target)الهبوط قبل الإطار المفتاحي قليلاًReadSample مرّة فقطحفظ إطار سابقكلّما طال GOP كبر الانحراف

الشكل 6: الـ seek يميل نحو إطار مفتاحي، فالقراءة مرّة قد تحفظ صورة أسبق بمقدار طول GOP.

4.2. ReadSample قد ينجح مع pSample == nullptr

ReadSample قد يعيد S_OK مع ppSample بقيمة NULL. عند النهاية يعود علم مثل MF_SOURCE_READERF_ENDOFSTREAM، وعند فجوة الدفق MF_SOURCE_READERF_STREAMTICK.

النظر إلى HRESULT فقط ثم الرجوع فوراً إلى pSample خطر. الأأمن النظر إلى HRESULT و flags و pSample كمجموعة ثلاثية.

التحقّق الثلاثي من نتيجة ReadSampleReadSample قد يعيد عيّنة NULL حتى مع S_OK، لذا افحص HRESULT و flags و pSample كمجموعة ثلاثية، وعالج أعلام النهاية أو فجوة الدفق.نتيجة ReadSampleانظر إلى HRESULTانظر إلى flagsانظر إلى pSampleقد يوجد علم نهاية أو فجوةقد يكون NULL حتى مع S_OK

الشكل 7: لا ترجع برمز النجاح وحده؛ افحص HRESULT و flags و pSample كمجموعة ثلاثية.

4.3. التعامل الفضفاض مع stride والاتّجاه الرأسي ينهار الصورة

مخزن الصورة ليس بالضرورة متراصّاً على width * bytesPerPixel. قد يدخل حشو في نهاية الصف، وقد تصير عائلة RGB من أسفل إلى أعلى. صفحة Microsoft Learn Image Stride و Uncompressed Video Buffers تكتب هذه النقطة بوضوح أيضاً.

المهمّتان خصوصاً التاليتان.

  • IMF2DBuffer::Lock2D يعيد مؤشّر بداية scan line 0 و stride الفعلي
  • في صورة bottom-up قد يصير stride سالباً

هذه المرّة نأخذ فكرة المساعد في Microsoft Learn، ونعيد الحزم في النهاية إلى مخزن BGRA متّصل من أعلى إلى أسفل ثم نمرّره إلى PNG. ترتيب هذا أوّلاً يبسّط جهة الحفظ كثيراً.

امتصاص stride والاتّجاهبـ Lock2D في IMF2DBuffer نأخذ مؤشّر بداية scan line 0 و stride الفعلي، بما فيه حالة bottom-up حيث يكون stride سالباً، ونعيد الحزم إلى مخزن BGRA متّصل من أعلى إلى أسفل ثم نمرّره إلى PNG.الأخذ بـ Lock2Dمؤشّر بداية scan line 0stride الفعليقد يصير سالباً في bottom-upإعادة الحزم إلى BGRA متّصل من أعلى إلى أسفلجهة الحفظ تصير بسيطة

الشكل 8: امتصّ stride الفعلي والاتّجاه بـ Lock2D، ورتّب إلى BGRA متّصل من أعلى إلى أسفل ثم مرّر إلى الحفظ.

4.4. لا تحسم البايت الرابع في MFVideoFormat_RGB32 على أنه alpha

MFVideoFormat_RGB32، بخلاف جوّ الاسم، ليس «RGBA نظيفاً» يُمرَّر كما هو إلى PNG. RGB بـ 32 بت على Windows بايتاته 0 و 1 و 2 هي B و G و R، والبايت 3 قد يكون alpha أو قد يُتجاهل. المهم أنه ليس ARGB32.

إن حسبته GUID_WICPixelFormat32bppBGRA وحفظته كما هو، فقد يدخل 0 في البايت الرابع وتصير الصورة شفافة على نحو غريب. هذه المرّة الاتجاه ملء alpha بـ 0xFF قبل الحفظ لجعله غير شفاف تماماً.

التعامل مع البايت الرابع في RGB32في إطار MFVideoFormat_RGB32 البايتات من 0 إلى 2 هي B و G و R والبايت الرابع ليس بالضرورة alpha، فكتابته كما هو إلى PNG قد تصير شفافة، والاتجاه ملء alpha بـ 0xFF قبل الحفظ لجعله غير شفاف.الحفظ كما هوالملء بـ 0xFF ثم الحفظإطار MFVideoFormat_RGB32البايت الرابع ليس بالضرورة alphaقد يصير PNG شفافاًPNG غير شفاف تماماً

الشكل 9: لا تحسم البايت الرابع؛ املأه بـ 0xFF قبل الحفظ وثبّته غير شفاف.

5. تدفّق التنفيذ

5.1. صنع Source Reader في وضع متزامن

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

عند صنع القارئ أربعة أمور.

  • MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  • إيقاف كل الدفقات مؤقّتاً
  • تشغيل MF_SOURCE_READER_FIRST_VIDEO_STREAM فقط
  • ضبط نوع الخرج على MFMediaType_Video / MFVideoFormat_RGB32

بهذا يسهل كتابة المرحلة اللاحقة بافتراض «نستقبل إطار RGB32».

أربع خطوات عند صنع القارئتفعيل معالجة الفيديو، ثم إيقاف كل الدفقات مؤقّتاً وتشغيل دفق الفيديو الأوّل فقط، ثم ضبط نوع الخرج على RGB32: ترتيب الخطوات الأربع عند صنع القارئ.تفعيل معالجة الفيديوإيقاف كل الدفقاتتشغيل دفق الفيديو الأوّل فقطضبط نوع الخرج على RGB32المرحلة اللاحقة تُكتب بافتراض RGB32

الشكل 10: إن أُنجزت الخطوات الأربع عند صنع القارئ بالترتيب، صارت المرحلة اللاحقة استقبال إطار RGB32 فقط.

5.2. بعد الـ seek نضيّق مع مراقبة timestamp

بعد SetCurrentPosition لا نحفظ فوراً. نقرأ العيّنات بـ ReadSample، ونقارن آخر إطار قبل الهدف مع أوّل إطار تجاوز الهدف.

هذه الخطوة الواحدة تمتصّ خشونة الـ seek كثيراً.

حوار مقارنة ما قبل وما بعد بعد الـ seekبعد seek التطبيق بـ SetCurrentPosition يكرّر ReadSample ويؤكّد timestamp، وعند تجاوز الهدف يقارن العيّنتين قبل وبعد ويختار الأقرب.Source ReaderالتطبيقSource Readerالتطبيقloop[حتى تجاوز الهدف]SetCurrentPosition(target)ReadSampleالعيّنة و timestampمقارنة العيّنتين قبل وبعد واعتماد الأقرب

الشكل 11: لا تحفظ فور الـ seek؛ تابع القراءة حتى تجاوز الهدف ثم قارن ما قبل وما بعد.

5.3. إعادة التشكيل من العيّنة إلى BGRA من أعلى إلى أسفل

العيّنة المستخرجة لا تُكتب كما هي إلى PNG، بل تُعاد حزمها مؤقّتاً في مخزن BGRA من أعلى إلى أسفل.

  • جعل المخزن واحداً بـ ConvertToContiguousBuffer
  • أخذ scan line 0 و stride الفعلي بمساعد BufferLock
  • النسخ صفاً صفاً إلى مخزن من أعلى إلى أسفل
  • جعل alpha بقيمة 0xFF

بهذا تستطيع جهة الحفظ معاملته كـ «صورة 32bpp BGRA عادية».

خطوات إعادة التشكيل من العيّنة إلى BGRAجعل المخزن واحداً بـ ConvertToContiguousBuffer، وأخذ scan line 0 و stride الفعلي بـ BufferLock، والنسخ صفاً صفاً إلى مخزن من أعلى إلى أسفل، وجعل alpha بقيمة 0xFF.ConvertToContiguousBufferscan line 0 و stride بـ BufferLockالنسخ صفاً صفاً من أعلى إلى أسفلجعل alpha بقيمة 0xFFصورة 32bpp BGRA عادية

الشكل 12: بعد أربع مراحل إعادة تشكيل تستطيع جهة الحفظ معاملته كصورة 32bpp BGRA عادية.

5.4. حفظ PNG يُترَك لـ WIC

الحفظ يستخدم IWICBitmapEncoder / IWICBitmapFrameEncode في WIC. التقسيم: Media Foundation يأخذ الإطار، و WIC يحوّله إلى صورة. هنا يكتمل بواجهات Windows القياسية فقط.

تقسيم العمل بين Media Foundation و WICMedia Foundation يتولّى أخذ الإطار، و WIC يتولّى التحويل إلى صورة والكتابة PNG.أخذ الإطارWIC يحوّل إلى صورةMedia Foundationإطار BGRAملف PNG

الشكل 13: أخذ الإطار لـ Media Foundation، والتحويل إلى صورة لـ WIC، ويكتمل بالواجهات القياسية فقط.

6. قائمة تحقّق للعمل اليومي

البند ما ننظر إليه ما يسهل وقوعه إن أُغفل
دقّة الـ seek لا تحسم بمرّة واحدة فور SetCurrentPosition حفظ إطار أسبق بكثير من الزمن المحدّد
NULL في العيّنة انظر إلى HRESULT و flags و pSample كلّها إلغاء مرجع فارغ عند النهاية أو stream tick
stride امتصّ stride الفعلي والاتّجاه الرأسي انهيار الصورة أو انقلابها رأسيّاً
البايت الرابع في RGB32 اجعل alpha بقيمة 0xFF PNG شفاف
نطاق الزمن احترم 0 <= target < duration سلوك غير مقصود قرب النهاية
استخراج متتالٍ كرّر الـ seek دون إعادة صنع القارئ بطء بلا داعٍ
عدد النسخ في معالجة كثيفة انتبه إلى تكلفة ConvertToContiguousBuffer استهلاك زائد لوحدة المعالجة ونطاق الذاكرة
تغيّر الصيغة فيديو خاص تتغيّر دقّته في الأثناء يحتاج تصميماً آخر تنكسر مقدّمة العرض والارتفاع

من هذه، صفّا «استخراج متتالٍ» و «عدد النسخ» وحدهما لا يخصّان استخراج إطار واحد نفسه، بل يظهران عند توسيع الأمر إلى عشرات الإطارات من الفيديو نفسه. عيّنة هذه المرّة تكوين يأخذ إطاراً وينتهي، فعند أخذ عدّة إطارات غيّر إلى تكرار SetCurrentPosition و ReadSample دون إعادة صنع Source Reader. خريطة Media Foundation الكاملة بما فيها خيارات غير Source Reader (Media Session وغيره) في مدخل إلى Media Foundation - فهم الواجهة من منظور COM.

توسيع استخراج إطار واحد إلى استخراج متتالٍلإطار واحد يكفي تكوين هذه المرّة كما هو، ولعدّة إطارات من الفيديو نفسه غيّر إلى تكرار الـ seek و ReadSample دون إعادة صنع Source Reader.إطار واحد فقطعدّة إطاراتكم إطاراً نأخذ؟تكوين هذه المرّة كما هوتكرار الـ seek والقراءة دون إعادة صنع القارئانتبه أيضاً إلى تكلفة عدد النسخ

الشكل 14: عند التوسيع إلى عدّة إطارات غيّر إلى تكرار الـ seek والقراءة دون إعادة صنع القارئ.

7. ملاحظات البناء والتشغيل

شيفرة نهاية هذه المقالة بشكل يسهل إضافتها كملف .cpp واحد إلى تطبيق console بـ C++ في Visual Studio. مقدّمات البيئة نفسها ملخَّصة في 2.2.

7.1. ملاحظات البناء

نقاط يسهل العمل إن ضُبطت.

  • وُضع #pragma comment(lib, ...) لذا إعدادات الرابط الإضافية غير لازمة أساساً
  • نستخدم wmain لذا تُعامَل وسائط سطر الأوامر كما هي بـ Unicode
  • حتى إن وُجد pch.h أو stdafx.h في قالب Console App الافتراضي، الشيفرة تلتقطهما في البداية بـ __has_include ليسهل اللصق
  • إن كان المشروع رغم ذلك يفرض ترويسة مترجمة مسبقاً خاصّة، يكفي ضبط هذا .cpp وحده على «عدم استخدام ترويسة مترجمة مسبقاً»
  • تكوين التشغيل يُفضَّل x64

7.2. طريقة التشغيل وما يظهر عند النجاح

الاستخدام ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>. مثلاً ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png.

عند النجاح تظهر في الخرج القياسي في نهاية wmain الأسطر الثلاثة التالية.

Saved: C:\work\frame.png
Requested: 12.345 sec
Actual: (زمن عرض الإطار المعتمد) sec

Requested هو الثواني الممرَّرة في الوسيط كما هي، و Actual هو timestamp الإطار المعتمد فعلاً. بقاعدة 3.2 يُؤخذ الأقرب من الإطارين قبل وبعد، لذا ينحصر الفرق بينهما تقريباً في نصف فاصل الإطار أو أقلّ (عند 29.97fps الحدّ الأعلى التقريبي نحو 17ms). إن ابتعد هنا مئات ms أو أكثر، اشتبه بأن مقارنة ما قبل وما بعد في 4.1 لا تعمل، كأن القراءة بعد الـ seek كانت مرّة واحدة فقط.

قراءة فرق Requested و Actualإن انحصر فرق Requested و Actual في نصف فاصل الإطار أو أقلّ فالأمر سليم، وإن ابتعد مئات الميلي ثانية أو أكثر فاشتبِه بأن مقارنة ما قبل وما بعد بعد الـ seek لا تعمل.نصف فاصل الإطار أو أقلّمئات ms أو أكثرانظر إلى فرق Requested و Actualمقارنة ما قبل وما بعد تعملاشتباه بأن مقارنة ما قبل وما بعد لا تعملتأكّد مثلاً من أن القراءة بعد الـ seek كانت مرّة واحدة فقط

الشكل 15: إن تجاوز الفرق نصف فاصل الإطار كثيراً فاشتبِه بتنفيذ مقارنة ما قبل وما بعد.

هدف التحقّق من التشغيل هذه الثلاث.

  • رمز الإنهاء 0، وسطر Saved: يحمل مسار الخرج المحدّد
  • عند فتح PNG الخارج تظهر مشهد ذلك الزمن بالاتّجاه الصحيح (غير منقلبة رأسيّاً)
  • عرض PNG وارتفاعه يطابقان دقّة الفيديو الأصلي، والخلفية غير شفافة (ملء alpha في 4.4 يعمل)

عند الفشل يظهر في الخطأ القياسي Failed. HRESULT = 0x......... اقرأ القيمة بأسلوب 2.1. إن كانت الثواني المحدّدة أكبر من طول الفيديو أو تساويه يصير 0x80070057 (E_INVALIDARG).

تدفّق التحقّق من نتيجة التشغيلعند نجاح التشغيل افحص الأسطر الثلاثة واتّجاه PNG ودقّته وعدم شفافيته، وعند الفشل اقرأ HRESULT في الخطأ القياسي، وإن كانت الثواني أكبر من طول الفيديو أو تساويه يصير E_INVALIDARG.نجاحفشلالتشغيلفحص الأسطر الثلاثة و PNGقراءة HRESULT في الخطأ القياسيالثواني أكبر من طول الفيديو أو تساويه تعني E_INVALIDARG

الشكل 16: عند النجاح افحص الخرج ونقاط PNG الثلاث، وعند الفشل اقرأ السبب من قيمة HRESULT.

8. الخلاصة

عند استخراج صورة ثابتة عند زمن محدّد من MP4 بـ Media Foundation، النظر إلى SetCurrentPosition و ReadSample وحدهما قليل قليلاً. عمليّاً،

  • الـ seek ليس exact
  • الإطار أفضل بمقارنة ما قبل وما بعد مع مراقبة timestamp
  • قد تنجح ReadSample دون عيّنة
  • امتصّ stride واتّجاه الصورة ثم احفظ
  • لا تحسم البايت الرابع في RGB32 على أنه alpha

إن ضبطت هذا المدى قلّ وقوع الحوادث كثيراً.

خمس نقاط تُضبطإن ضُبطت خمس نقاط: الـ seek ليس exact، ومقارنة timestamp قبل وبعد، وفحص غياب العيّنة حتى عند النجاح، وامتصاص stride والاتّجاه، وعدم حسم البايت الرابع في RGB32، قلّ وقوع الحوادث.الـ seek ليس exactمقارنة timestamp قبل وبعدفحص غياب العيّنة حتى عند النجاحامتصاص stride والاتّجاهعدم حسم البايت الرابعتنفيذ أقلّ عرضة للحوادث

الشكل 17: باكتمال نقاط الضبط الخمس يصير استخراج الصورة الثابتة عند زمن محدّد أقلّ عرضة للحوادث بكثير.

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

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

10. شيفرة كاملة تُلصَق كما هي في .cpp

الكتلة التالية شيفرة مقدّمتها نقلها كما هي إلى مشروع تطبيق console بـ C++ في Visual Studio. وسائط سطر الأوامر بالترتيب input.mp4 ثم seconds ثم output.png. التكوين ملف واحد ليُلصَق بسهولة في المشروع.

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

الدالة أو الصنف في الشيفرة المتن المقابل الدور والفخّ الذي تعالجه
MediaFoundationScope 5.1 جمع تهيئة CoInitializeEx و MFStartup والتنظيف
CreateConfiguredSourceReader 5.1 صنع القارئ، واختيار دفق الفيديو فقط، وطلب MFVideoFormat_RGB32
GetPresentationDuration 6. (نطاق الزمن) أخذ طول الفيديو والتأكّد من 0 <= target < duration
SeekSourceReader 4.1 / 5.2 الـ seek بـ SetCurrentPosition. هذا وحده لا يصير exact
ReadNearestVideoSample 3.2 / 4.1 / 4.2 / 5.2 مقارنة ما قبل الهدف وما بعده واختيار إطار واحد. تعالج هنا أيضاً flags و null في pSample
GetDefaultStride 4.3 حساب stride وتعويضه عندما يغيب MF_MT_DEFAULT_STRIDE
BufferLock 4.3 / 5.3 أخذ scan line 0 و stride الفعلي بـ IMF2DBuffer::Lock2D
CopyContiguousBufferToTopDownBgra 4.3 / 4.4 / 5.3 إعادة الحزم صفاً صفاً إلى مخزن من أعلى إلى أسفل، وملء البايت الرابع بـ 0xFF
CopySampleToTopDownBgra 5.3 أخذ حجم الإطار و stride واستدعاء النسخ أعلاه
SaveBgraToPng 5.4 كتابة 32bpp BGRA كـ PNG عبر WIC
ExtractFrameFromMp4ToPng 5. ككلّ مدخل يستدعي الدوال أعلاه بالترتيب
TryParseSeconds / wmain 7.2 تحليل الوسائط وعرض Requested / Actual
#define NOMINMAX
#if defined(_MSC_VER)
#  if __has_include("pch.h")
#    include "pch.h"
#  elif __has_include("stdafx.h")
#    include "stdafx.h"
#  endif
#endif
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <mfobjects.h>
#include <propvarutil.h>
#include <wincodec.h>

#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cwchar>
#include <cmath>
#include <cstring>
#include <limits>
#include <vector>

#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "propsys.lib")
#pragma comment(lib, "windowscodecs.lib")

template <class T>
void SafeRelease(T** pp)
{
    if (pp != nullptr && *pp != nullptr)
    {
        (*pp)->Release();
        *pp = nullptr;
    }
}

class MediaFoundationScope
{
public:
    MediaFoundationScope() : m_comInitialized(false), m_mfStarted(false)
    {
    }

    HRESULT Initialize()
    {
        HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
        if (hr == RPC_E_CHANGED_MODE)
        {
            return hr;
        }

        if (SUCCEEDED(hr))
        {
            m_comInitialized = true;
        }

        hr = MFStartup(MF_VERSION);
        if (FAILED(hr))
        {
            if (m_comInitialized)
            {
                CoUninitialize();
                m_comInitialized = false;
            }
            return hr;
        }

        m_mfStarted = true;
        return S_OK;
    }

    ~MediaFoundationScope()
    {
        if (m_mfStarted)
        {
            MFShutdown();
        }

        if (m_comInitialized)
        {
            CoUninitialize();
        }
    }

private:
    bool m_comInitialized;
    bool m_mfStarted;
};

HRESULT GetPresentationDuration(IMFSourceReader* pReader, LONGLONG* phnsDuration)
{
    if (pReader == nullptr || phnsDuration == nullptr)
    {
        return E_POINTER;
    }

    PROPVARIANT var;
    PropVariantInit(&var);

    HRESULT hr = pReader->GetPresentationAttribute(
        MF_SOURCE_READER_MEDIASOURCE,
        MF_PD_DURATION,
        &var);

    if (SUCCEEDED(hr))
    {
        hr = PropVariantToInt64(var, phnsDuration);
    }

    PropVariantClear(&var);
    return hr;
}

HRESULT GetDefaultStride(IMFMediaType* pType, LONG* plStride)
{
    if (pType == nullptr || plStride == nullptr)
    {
        return E_POINTER;
    }

    LONG lStride = 0;
    HRESULT hr = pType->GetUINT32(
        MF_MT_DEFAULT_STRIDE,
        reinterpret_cast<UINT32*>(&lStride));

    if (FAILED(hr))
    {
        GUID subtype = GUID_NULL;
        UINT32 width = 0;
        UINT32 height = 0;

        hr = pType->GetGUID(MF_MT_SUBTYPE, &subtype);
        if (FAILED(hr))
        {
            return hr;
        }

        hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &width, &height);
        if (FAILED(hr))
        {
            return hr;
        }

        hr = MFGetStrideForBitmapInfoHeader(subtype.Data1, width, &lStride);
        if (FAILED(hr))
        {
            return hr;
        }

        (void)pType->SetUINT32(MF_MT_DEFAULT_STRIDE, static_cast<UINT32>(lStride));
    }

    *plStride = lStride;
    return S_OK;
}

class BufferLock
{
public:
    explicit BufferLock(IMFMediaBuffer* pBuffer)
        : m_pBuffer(pBuffer),
          m_p2DBuffer(nullptr),
          m_locked(false)
    {
        if (m_pBuffer != nullptr)
        {
            m_pBuffer->AddRef();
            (void)m_pBuffer->QueryInterface(IID_PPV_ARGS(&m_p2DBuffer));
        }
    }

    ~BufferLock()
    {
        UnlockBuffer();
        SafeRelease(&m_p2DBuffer);
        SafeRelease(&m_pBuffer);
    }

    HRESULT LockBuffer(
        LONG defaultStride,
        DWORD heightInPixels,
        BYTE** ppScanLine0,
        LONG* plStride)
    {
        if (ppScanLine0 == nullptr || plStride == nullptr)
        {
            return E_POINTER;
        }

        *ppScanLine0 = nullptr;
        *plStride = 0;

        HRESULT hr = S_OK;

        if (m_p2DBuffer != nullptr)
        {
            hr = m_p2DBuffer->Lock2D(ppScanLine0, plStride);
        }
        else
        {
            BYTE* pData = nullptr;
            hr = m_pBuffer->Lock(&pData, nullptr, nullptr);
            if (SUCCEEDED(hr))
            {
                *plStride = defaultStride;

                if (defaultStride < 0)
                {
                    const size_t strideAbs = static_cast<size_t>(-defaultStride);
                    *ppScanLine0 = pData + strideAbs * (heightInPixels - 1);
                }
                else
                {
                    *ppScanLine0 = pData;
                }
            }
        }

        m_locked = SUCCEEDED(hr);
        return hr;
    }

    void UnlockBuffer()
    {
        if (!m_locked)
        {
            return;
        }

        if (m_p2DBuffer != nullptr)
        {
            (void)m_p2DBuffer->Unlock2D();
        }
        else if (m_pBuffer != nullptr)
        {
            (void)m_pBuffer->Unlock();
        }

        m_locked = false;
    }

private:
    IMFMediaBuffer* m_pBuffer;
    IMF2DBuffer* m_p2DBuffer;
    bool m_locked;
};

HRESULT CreateConfiguredSourceReader(PCWSTR inputPath, IMFSourceReader** ppReader)
{
    if (inputPath == nullptr || ppReader == nullptr)
    {
        return E_POINTER;
    }

    *ppReader = nullptr;

    IMFAttributes* pAttributes = nullptr;
    IMFSourceReader* pReader = nullptr;
    IMFMediaType* pRequestedType = nullptr;

    HRESULT hr = MFCreateAttributes(&pAttributes, 1);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = MFCreateSourceReaderFromURL(inputPath, pAttributes, &pReader);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = MFCreateMediaType(&pRequestedType);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pRequestedType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pRequestedType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pReader->SetCurrentMediaType(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        nullptr,
        pRequestedType);
    if (FAILED(hr))
    {
        goto done;
    }

    *ppReader = pReader;
    pReader = nullptr;

done:
    SafeRelease(&pRequestedType);
    SafeRelease(&pReader);
    SafeRelease(&pAttributes);
    return hr;
}

HRESULT SeekSourceReader(IMFSourceReader* pReader, LONGLONG targetHns)
{
    if (pReader == nullptr)
    {
        return E_POINTER;
    }

    PROPVARIANT var;
    PropVariantInit(&var);

    HRESULT hr = InitPropVariantFromInt64(targetHns, &var);
    if (SUCCEEDED(hr))
    {
        hr = pReader->SetCurrentPosition(GUID_NULL, var);
    }

    PropVariantClear(&var);
    return hr;
}

HRESULT ReadNearestVideoSample(
    IMFSourceReader* pReader,
    LONGLONG targetHns,
    IMFSample** ppSample,
    LONGLONG* pChosenTimestampHns)
{
    if (pReader == nullptr || ppSample == nullptr)
    {
        return E_POINTER;
    }

    *ppSample = nullptr;
    if (pChosenTimestampHns != nullptr)
    {
        *pChosenTimestampHns = 0;
    }

    IMFSample* pBefore = nullptr;
    LONGLONG beforeTimestamp = 0;
    bool hasBefore = false;

    HRESULT hr = S_OK;

    for (;;)
    {
        IMFSample* pCurrent = nullptr;
        DWORD flags = 0;
        LONGLONG currentTimestamp = 0;
        LONGLONG diffBefore = 0;
        LONGLONG diffCurrent = 0;

        hr = pReader->ReadSample(
            MF_SOURCE_READER_FIRST_VIDEO_STREAM,
            0,
            nullptr,
            &flags,
            &currentTimestamp,
            &pCurrent);

        if (FAILED(hr))
        {
            SafeRelease(&pCurrent);
            break;
        }

        if ((flags & MF_SOURCE_READERF_ENDOFSTREAM) != 0)
        {
            SafeRelease(&pCurrent);

            if (hasBefore)
            {
                *ppSample = pBefore;
                pBefore = nullptr;

                if (pChosenTimestampHns != nullptr)
                {
                    *pChosenTimestampHns = beforeTimestamp;
                }

                hr = S_OK;
            }
            else
            {
                hr = MF_E_END_OF_STREAM;
            }
            break;
        }

        if ((flags & MF_SOURCE_READERF_STREAMTICK) != 0)
        {
            SafeRelease(&pCurrent);
            continue;
        }

        if (pCurrent == nullptr)
        {
            continue;
        }

        if (currentTimestamp < targetHns)
        {
            SafeRelease(&pBefore);
            pBefore = pCurrent;
            pCurrent = nullptr;
            beforeTimestamp = currentTimestamp;
            hasBefore = true;
            continue;
        }

        if (hasBefore)
        {
            diffBefore = targetHns - beforeTimestamp;
            diffCurrent = currentTimestamp - targetHns;

            if (diffBefore <= diffCurrent)
            {
                *ppSample = pBefore;
                pBefore = nullptr;

                if (pChosenTimestampHns != nullptr)
                {
                    *pChosenTimestampHns = beforeTimestamp;
                }

                SafeRelease(&pCurrent);
            }
            else
            {
                *ppSample = pCurrent;
                pCurrent = nullptr;

                if (pChosenTimestampHns != nullptr)
                {
                    *pChosenTimestampHns = currentTimestamp;
                }
            }
        }
        else
        {
            *ppSample = pCurrent;
            pCurrent = nullptr;

            if (pChosenTimestampHns != nullptr)
            {
                *pChosenTimestampHns = currentTimestamp;
            }
        }

        hr = S_OK;
        break;
    }

    SafeRelease(&pBefore);
    return hr;
}

HRESULT CopyContiguousBufferToTopDownBgra(
    IMFMediaBuffer* pBuffer,
    LONG defaultStride,
    UINT32 width,
    UINT32 height,
    std::vector<BYTE>& pixels,
    UINT32* pStride)
{
    if (pBuffer == nullptr || pStride == nullptr)
    {
        return E_POINTER;
    }

    BufferLock lock(pBuffer);

    BYTE* pScanLine0 = nullptr;
    LONG actualStride = 0;

    HRESULT hr = lock.LockBuffer(defaultStride, height, &pScanLine0, &actualStride);
    if (FAILED(hr))
    {
        return hr;
    }

    if (width > (std::numeric_limits<UINT32>::max() / 4))
    {
        return E_INVALIDARG;
    }

    const UINT32 destStride = width * 4;
    const LONG actualStrideAbs = (actualStride < 0) ? -actualStride : actualStride;
    if (actualStrideAbs < static_cast<LONG>(destStride))
    {
        return E_UNEXPECTED;
    }

    pixels.resize(static_cast<size_t>(destStride) * height);

    BYTE* pDestRow = pixels.data();
    BYTE* pSrcRow = pScanLine0;

    for (UINT32 y = 0; y < height; ++y)
    {
        std::memcpy(pDestRow, pSrcRow, destStride);

        // MFVideoFormat_RGB32 の 4 byte 目は alpha と限らないので、
        // PNG 保存前に不透明へ固定する。
        for (UINT32 x = 0; x < width; ++x)
        {
            pDestRow[static_cast<size_t>(x) * 4 + 3] = 0xFF;
        }

        pDestRow += destStride;
        pSrcRow += actualStride;
    }

    *pStride = destStride;
    return S_OK;
}

HRESULT CopySampleToTopDownBgra(
    IMFSample* pSample,
    IMFMediaType* pCurrentType,
    std::vector<BYTE>& pixels,
    UINT32* pWidth,
    UINT32* pHeight,
    UINT32* pStride)
{
    if (pSample == nullptr || pCurrentType == nullptr ||
        pWidth == nullptr || pHeight == nullptr || pStride == nullptr)
    {
        return E_POINTER;
    }

    *pWidth = 0;
    *pHeight = 0;
    *pStride = 0;

    IMFMediaBuffer* pBuffer = nullptr;

    GUID subtype = GUID_NULL;
    UINT32 width = 0;
    UINT32 height = 0;
    LONG defaultStride = 0;

    HRESULT hr = pCurrentType->GetGUID(MF_MT_SUBTYPE, &subtype);
    if (FAILED(hr))
    {
        goto done;
    }

    if (!IsEqualGUID(subtype, MFVideoFormat_RGB32))
    {
        hr = MF_E_INVALIDMEDIATYPE;
        goto done;
    }

    hr = MFGetAttributeSize(pCurrentType, MF_MT_FRAME_SIZE, &width, &height);
    if (FAILED(hr))
    {
        goto done;
    }

    if (width == 0 || height == 0)
    {
        hr = E_UNEXPECTED;
        goto done;
    }

    hr = GetDefaultStride(pCurrentType, &defaultStride);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pSample->ConvertToContiguousBuffer(&pBuffer);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = CopyContiguousBufferToTopDownBgra(
        pBuffer,
        defaultStride,
        width,
        height,
        pixels,
        pStride);
    if (FAILED(hr))
    {
        goto done;
    }

    *pWidth = width;
    *pHeight = height;

    hr = S_OK;

done:
    SafeRelease(&pBuffer);
    return hr;
}

HRESULT SaveBgraToPng(
    PCWSTR outputPath,
    const BYTE* pixels,
    UINT32 width,
    UINT32 height,
    UINT32 stride)
{
    if (outputPath == nullptr || pixels == nullptr)
    {
        return E_POINTER;
    }

    if (width == 0 || height == 0 || stride < width * 4)
    {
        return E_INVALIDARG;
    }

    const size_t bufferSizeSizeT = static_cast<size_t>(stride) * height;
    if (bufferSizeSizeT > static_cast<size_t>(std::numeric_limits<UINT>::max()))
    {
        return E_INVALIDARG;
    }

    const UINT bufferSize = static_cast<UINT>(bufferSizeSizeT);

    IWICImagingFactory* pFactory = nullptr;
    IWICStream* pStream = nullptr;
    IWICBitmapEncoder* pEncoder = nullptr;
    IWICBitmapFrameEncode* pFrame = nullptr;
    IPropertyBag2* pProps = nullptr;
    WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppBGRA;

    HRESULT hr = CoCreateInstance(
        CLSID_WICImagingFactory,
        nullptr,
        CLSCTX_INPROC_SERVER,
        IID_PPV_ARGS(&pFactory));
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFactory->CreateStream(&pStream);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pStream->InitializeFromFilename(outputPath, GENERIC_WRITE);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &pEncoder);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->Initialize(pStream, WICBitmapEncoderNoCache);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->CreateNewFrame(&pFrame, &pProps);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFrame->Initialize(pProps);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFrame->SetSize(width, height);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFrame->SetPixelFormat(&pixelFormat);
    if (FAILED(hr))
    {
        goto done;
    }

    if (!IsEqualGUID(pixelFormat, GUID_WICPixelFormat32bppBGRA))
    {
        hr = WINCODEC_ERR_UNSUPPORTEDPIXELFORMAT;
        goto done;
    }

    hr = pFrame->WritePixels(
        height,
        stride,
        bufferSize,
        const_cast<BYTE*>(pixels));
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFrame->Commit();
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->Commit();

done:
    SafeRelease(&pProps);
    SafeRelease(&pFrame);
    SafeRelease(&pEncoder);
    SafeRelease(&pStream);
    SafeRelease(&pFactory);
    return hr;
}

HRESULT ExtractFrameFromMp4ToPng(
    PCWSTR inputPath,
    LONGLONG targetHns,
    PCWSTR outputPath,
    LONGLONG* pActualTimestampHns)
{
    if (inputPath == nullptr || outputPath == nullptr)
    {
        return E_POINTER;
    }

    if (targetHns < 0)
    {
        return E_INVALIDARG;
    }

    MediaFoundationScope mf;
    HRESULT hr = mf.Initialize();
    if (FAILED(hr))
    {
        return hr;
    }

    IMFSourceReader* pReader = nullptr;
    IMFMediaType* pCurrentType = nullptr;
    IMFSample* pChosenSample = nullptr;

    LONGLONG durationHns = 0;
    UINT32 width = 0;
    UINT32 height = 0;
    UINT32 stride = 0;
    std::vector<BYTE> pixels;

    hr = CreateConfiguredSourceReader(inputPath, &pReader);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pReader->GetCurrentMediaType(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        &pCurrentType);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = GetPresentationDuration(pReader, &durationHns);
    if (FAILED(hr))
    {
        goto done;
    }

    if (targetHns >= durationHns)
    {
        hr = E_INVALIDARG;
        goto done;
    }

    hr = SeekSourceReader(pReader, targetHns);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = ReadNearestVideoSample(
        pReader,
        targetHns,
        &pChosenSample,
        pActualTimestampHns);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = CopySampleToTopDownBgra(
        pChosenSample,
        pCurrentType,
        pixels,
        &width,
        &height,
        &stride);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = SaveBgraToPng(outputPath, pixels.data(), width, height, stride);

done:
    SafeRelease(&pChosenSample);
    SafeRelease(&pCurrentType);
    SafeRelease(&pReader);
    return hr;
}

bool TryParseSeconds(PCWSTR text, LONGLONG* phns)
{
    if (text == nullptr || phns == nullptr)
    {
        return false;
    }

    wchar_t* end = nullptr;
    errno = 0;

    const double seconds = std::wcstod(text, &end);
    if (end == text || *end != L'\0' || errno != 0)
    {
        return false;
    }

    if (!std::isfinite(seconds) || seconds < 0.0)
    {
        return false;
    }

    const long double hns =
        static_cast<long double>(seconds) * 10000000.0L;

    if (hns < 0.0L ||
        hns > static_cast<long double>(std::numeric_limits<LONGLONG>::max()))
    {
        return false;
    }

    *phns = static_cast<LONGLONG>(std::llround(hns));
    return true;
}

double HnsToSeconds(LONGLONG hns)
{
    return static_cast<double>(hns) / 10000000.0;
}

void PrintUsage()
{
    std::fwprintf(stderr, L"Usage:\n");
    std::fwprintf(stderr, L"  ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>\n");
    std::fwprintf(stderr, L"\nExample:\n");
    std::fwprintf(stderr, L"  ExtractFrameFromMp4.exe input.mp4 12.345 output.png\n");
}

int wmain(int argc, wchar_t* argv[])
{
    if (argc != 4)
    {
        PrintUsage();
        return 1;
    }

    LONGLONG targetHns = 0;
    if (!TryParseSeconds(argv[2], &targetHns))
    {
        std::fwprintf(stderr, L"Invalid seconds: %ls\n", argv[2]);
        return 1;
    }

    LONGLONG actualHns = 0;
    HRESULT hr = ExtractFrameFromMp4ToPng(
        argv[1],
        targetHns,
        argv[3],
        &actualHns);

    if (FAILED(hr))
    {
        std::fwprintf(stderr, L"Failed. HRESULT = 0x%08lX\n", static_cast<unsigned long>(hr));
        return 1;
    }

    std::wprintf(L"Saved: %ls\n", argv[3]);
    std::wprintf(L"Requested: %.3f sec\n", HnsToSeconds(targetHns));
    std::wprintf(L"Actual: %.3f sec\n", HnsToSeconds(actualHns));
    return 0;
}

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

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

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

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

ماذا نستخدم لاستخراج صورة ثابتة عند زمن محدّد من MP4؟
لإطار واحد يكون IMFSourceReader أوضح كمدخل من Media Session. التدفّق: فتح الملف بـ MFCreateSourceReaderFromURL، واختيار الفيديو فقط بـ SetStreamSelection، وطلب MFVideoFormat_RGB32، والـ seek بـ SetCurrentPosition، وأخذ الإطار بـ ReadSample، ثم الحفظ PNG عبر WIC. يكتمل بواجهات Windows القياسية دون مكتبة خارجية.
هل ينتقل SetCurrentPosition إلى الزمن المحدّد تماماً؟
لا. IMFSourceReader::SetCurrentPosition لا يضمن exact seeking، وفي الفيديو يميل عادة إلى ما قبل الموضع المحدّد قليلاً، خصوصاً نحو إطار مفتاحي. بعد الـ seek يلزم التقدّم بـ ReadSample ومراقبة timestamp، ومقارنة آخر عيّنة قبل الهدف مع أوّل عيّنة بعده، واعتماد الأقرب. تنفيذ يقرأ مرّة واحدة بعد الـ seek ثم يحفظ ينحرف بشكل عادي في فيديو GOP طويل.
لماذا يصير PNG المحفوظ شفافاً؟
لأن البايت الرابع في MFVideoFormat_RGB32 ليس بالضرورة alpha. RGB بـ 32 بت على Windows بايتاته 0 و 1 و 2 هي B و G و R، والبايت 3 قد يكون alpha أو قد يُتجاهل (وليس ARGB32). كتابته كما هو إلى PNG قد يعطي صورة شفافة، لذا الأأمن ملء البايت الرابع بـ 0xFF قبل الحفظ لجعله غير شفاف.
ما سبب انهيار الصورة أو انقلابها رأسيّاً؟
التعامل مع stride والاتّجاه الرأسي. مخزن الصورة ليس بالضرورة متراصّاً على width×bytesPerPixel، فقد يدخل حشو في نهاية الصف، وقد تصير عائلة RGB من أسفل إلى أعلى (stride سالب). خذ مؤشّر بداية scan line 0 و stride الفعلي بـ IMF2DBuffer::Lock2D، وأعد الحزم مرّة في مخزن BGRA متّصل من أعلى إلى أسفل ثم مرّره إلى PNG، فيبسط جهة الحفظ ويمنع الانهيار.

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

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

غو كومورا

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

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

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