استخراج صورة ثابتة من 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 شفاف على نحو غريب.
flowchart TB
accTitle: حوادث التقدّم الفضفاض
accDescr: تنفيذ يسعى ثم يقرأ مرّة ويحفظ يتداخل فيه الإطار المفتاحي و timestamp و stride والاتّجاه الرأسي والبايت الرابع في RGB32، فتقع حوادث انحراف الزمن والانقلاب الرأسي و PNG الشفاف.
rough1["seek ثم قراءة مرّة ثم حفظ"] --> tz1["انحراف الزمن قليلاً"]
rough1 --> ud1["انقلاب الصورة رأسيّاً"]
rough1 --> tp1["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 حتى يثبت الأمر كثيراً.
flowchart TB
accTitle: التدفّق الفضفاض والتدفّق الذي يثبت
accDescr: تدفّق seek ثم قراءة مرّة ثم حفظ فضفاض، أمّا التدفّق الذي يقارن ما قبل وما بعد مع مراقبة timestamp بعد الـ seek ثم ينسخ بمراعاة stride ثم يحفظ PNG فيثبت كثيراً.
sk1["seek"] --> cmp1["مقارنة ما قبل وما بعد مع مراقبة timestamp"]
cmp1 --> cp1["نسخ يراعي stride"]
cp1 --> sv1["حفظ PNG"]
sk1 -.->|"القراءة مرّة ثم الحفظ فضفاض"| ng1["سبب الانحراف والانهيار"]
الشكل 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 عامّاً لا تتغيّر فيه الدقّة في الأثناء
للتشغيل ومزامنة الصوت وشريط التمرير وربط واجهة المستخدم تصميم آخر، غير أنه لغرض إطار واحد هذا أوضح كثيراً.
flowchart TB
accTitle: تركيب المقدّمات هذه المرّة
accDescr: إدخال MP4 محلّي، وأخذ الإطار الأقرب إلى زمن محدّد بـ IMFSourceReader متزامن، والحفظ PNG عبر WIC، تكوين بلا مكتبة خارجية.
mp1["MP4 محلّي"] --> rd1["IMFSourceReader متزامن"]
rd1 --> nf1["الإطار الأقرب إلى الزمن المحدّد"]
nf1 --> png1["حفظ PNG عبر WIC"]
rd1 -.-> std1["يكتمل بواجهات 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نقارن فرق العيّنة السابقة والعيّنة الحالية - نعتمد الأقرب إلى الهدف
بهذا يسهل أخذ الإطار الأقرب إلى الهدف لا «أوّل إطار بعد الهدف».
flowchart TB
accTitle: قاعدة حكم اختيار الإطار الأقرب
accDescr: بعد الـ seek نتقدّم بـ ReadSample، ونحتفظ بآخر عيّنة قبل الهدف، وعندما تأتي أوّل عيّنة بعده نقارن فرق الاثنتين ونعتمد الأقرب إلى الهدف.
adv1["التقدّم بـ ReadSample بعد الـ seek"] --> bf1["الاحتفاظ بآخر عيّنة قبل الهدف"]
bf1 --> af1["تأتي أوّل عيّنة بعد الهدف"]
af1 --> df1["مقارنة فرق كلّ منهما عن الهدف"]
df1 --> pk1["اعتماد الأقرب"]
الشكل 4: مقارنة مرشّحين قبل الهدف وبعده ثم الاعتماد يأخذ الإطار الأقرب لا «أوّل إطار بعد الهدف».
3.3. صورة المعالجة
من رأس المعالجة إلى ذيلها تقريباً: input.mp4 -> صنع Source Reader -> طلب RGB32 -> seek -> تكرار ReadSample -> مقارنة ما قبل الهدف وما بعده -> إعادة الحزم إلى BGRA من أعلى إلى أسفل -> حفظ PNG عبر WIC.
المظهر بسيط، غير أن دقّة الـ seek و null في العيّنة و stride والتعامل مع البايت الرابع لكلّ منها فخّ صغير. إن لم تطأ هذه الأربعة استقرّ التنفيذ نفسه بوضوح.
flowchart TB
accTitle: أربعة فخاخ كامنة في المعالجة
accDescr: مظهر المعالجة بسيط، غير أن دقّة الـ seek و null في العيّنة و stride والبايت الرابع في RGB32 أربعة فخاخ صغيرة، وإن لم تُوطأ استقرّ التنفيذ بوضوح.
lk1["معالجة بسيطة المظهر"] --> t1["دقّة الـ seek"]
lk1 --> t2["null في العيّنة"]
lk1 --> t3["stride والاتّجاه"]
t3 -.-> t4["التعامل مع البايت الرابع"]
t1 --> okf["إن لم تُوطأ استقرّ بوضوح"]
t2 --> okf
t3 --> okf
الشكل 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 طويل ينحرف بهذا. إن كان فاصل الإطار المفتاحي ثانيتين، فقد تحفظ إطاراً أقرب إلى ثانيتين قبل الزمن المحدّد في الحدّ الأقصى.
flowchart TB
accTitle: آلية انحراف القراءة مرّة فور الـ seek
accDescr: SetCurrentPosition يهبط قبل الموضع المحدّد قليلاً خصوصاً نحو إطار مفتاحي، فإن قرأت مرّة فور ذلك وحفظت فقد تحفظ في فيديو GOP طويل إطاراً أسبق بكثير من الزمن المحدّد.
sp1["SetCurrentPosition(target)"] --> kf1["الهبوط قبل الإطار المفتاحي قليلاً"]
kf1 --> one1["ReadSample مرّة فقط"]
one1 --> ng2["حفظ إطار سابق"]
ng2 -.-> gp1["كلّما طال 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 كمجموعة ثلاثية.
flowchart TB
accTitle: التحقّق الثلاثي من نتيجة ReadSample
accDescr: ReadSample قد يعيد عيّنة NULL حتى مع S_OK، لذا افحص HRESULT و flags و pSample كمجموعة ثلاثية، وعالج أعلام النهاية أو فجوة الدفق.
rs1["نتيجة ReadSample"] --> h1["انظر إلى HRESULT"]
rs1 --> f1["انظر إلى flags"]
rs1 --> s1["انظر إلى pSample"]
f1 -.-> gap1["قد يوجد علم نهاية أو فجوة"]
s1 -.-> nl1["قد يكون 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. ترتيب هذا أوّلاً يبسّط جهة الحفظ كثيراً.
flowchart TB
accTitle: امتصاص stride والاتّجاه
accDescr: بـ Lock2D في IMF2DBuffer نأخذ مؤشّر بداية scan line 0 و stride الفعلي، بما فيه حالة bottom-up حيث يكون stride سالباً، ونعيد الحزم إلى مخزن BGRA متّصل من أعلى إلى أسفل ثم نمرّره إلى PNG.
l2d["الأخذ بـ Lock2D"] --> sl0["مؤشّر بداية scan line 0"]
l2d --> ast1["stride الفعلي"]
ast1 -.-> neg1["قد يصير سالباً في bottom-up"]
sl0 --> pack1["إعادة الحزم إلى BGRA متّصل من أعلى إلى أسفل"]
ast1 --> pack1
pack1 --> sim1["جهة الحفظ تصير بسيطة"]
الشكل 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 قبل الحفظ لجعله غير شفاف تماماً.
flowchart TB
accTitle: التعامل مع البايت الرابع في RGB32
accDescr: في إطار MFVideoFormat_RGB32 البايتات من 0 إلى 2 هي B و G و R والبايت الرابع ليس بالضرورة alpha، فكتابته كما هو إلى PNG قد تصير شفافة، والاتجاه ملء alpha بـ 0xFF قبل الحفظ لجعله غير شفاف.
rgb1["إطار MFVideoFormat_RGB32"] --> b4["البايت الرابع ليس بالضرورة alpha"]
b4 -->|"الحفظ كما هو"| tp2["قد يصير PNG شفافاً"]
b4 -->|"الملء بـ 0xFF ثم الحفظ"| op1["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».
flowchart TB
accTitle: أربع خطوات عند صنع القارئ
accDescr: تفعيل معالجة الفيديو، ثم إيقاف كل الدفقات مؤقّتاً وتشغيل دفق الفيديو الأوّل فقط، ثم ضبط نوع الخرج على RGB32: ترتيب الخطوات الأربع عند صنع القارئ.
c1["تفعيل معالجة الفيديو"] --> c2["إيقاف كل الدفقات"]
c2 --> c3["تشغيل دفق الفيديو الأوّل فقط"]
c3 --> c4["ضبط نوع الخرج على RGB32"]
c4 -.-> rdy1["المرحلة اللاحقة تُكتب بافتراض RGB32"]
الشكل 10: إن أُنجزت الخطوات الأربع عند صنع القارئ بالترتيب، صارت المرحلة اللاحقة استقبال إطار RGB32 فقط.
5.2. بعد الـ seek نضيّق مع مراقبة timestamp
بعد SetCurrentPosition لا نحفظ فوراً. نقرأ العيّنات بـ ReadSample، ونقارن آخر إطار قبل الهدف مع أوّل إطار تجاوز الهدف.
هذه الخطوة الواحدة تمتصّ خشونة الـ seek كثيراً.
sequenceDiagram
accTitle: حوار مقارنة ما قبل وما بعد بعد الـ seek
accDescr: بعد seek التطبيق بـ SetCurrentPosition يكرّر ReadSample ويؤكّد timestamp، وعند تجاوز الهدف يقارن العيّنتين قبل وبعد ويختار الأقرب.
participant A as التطبيق
participant R as Source Reader
A->>R: SetCurrentPosition(target)
loop حتى تجاوز الهدف
A->>R: ReadSample
R-->>A: العيّنة و timestamp
end
A->>A: مقارنة العيّنتين قبل وبعد واعتماد الأقرب
الشكل 11: لا تحفظ فور الـ seek؛ تابع القراءة حتى تجاوز الهدف ثم قارن ما قبل وما بعد.
5.3. إعادة التشكيل من العيّنة إلى BGRA من أعلى إلى أسفل
العيّنة المستخرجة لا تُكتب كما هي إلى PNG، بل تُعاد حزمها مؤقّتاً في مخزن BGRA من أعلى إلى أسفل.
- جعل المخزن واحداً بـ
ConvertToContiguousBuffer - أخذ scan line 0 و stride الفعلي بمساعد
BufferLock - النسخ صفاً صفاً إلى مخزن من أعلى إلى أسفل
- جعل alpha بقيمة
0xFF
بهذا تستطيع جهة الحفظ معاملته كـ «صورة 32bpp BGRA عادية».
flowchart TB
accTitle: خطوات إعادة التشكيل من العيّنة إلى BGRA
accDescr: جعل المخزن واحداً بـ ConvertToContiguousBuffer، وأخذ scan line 0 و stride الفعلي بـ BufferLock، والنسخ صفاً صفاً إلى مخزن من أعلى إلى أسفل، وجعل alpha بقيمة 0xFF.
cv1["ConvertToContiguousBuffer"] --> bl1["scan line 0 و stride بـ BufferLock"]
bl1 --> rc1["النسخ صفاً صفاً من أعلى إلى أسفل"]
rc1 --> al1["جعل alpha بقيمة 0xFF"]
al1 --> out1["صورة 32bpp BGRA عادية"]
الشكل 12: بعد أربع مراحل إعادة تشكيل تستطيع جهة الحفظ معاملته كصورة 32bpp BGRA عادية.
5.4. حفظ PNG يُترَك لـ WIC
الحفظ يستخدم IWICBitmapEncoder / IWICBitmapFrameEncode في WIC. التقسيم: Media Foundation يأخذ الإطار، و WIC يحوّله إلى صورة. هنا يكتمل بواجهات Windows القياسية فقط.
flowchart LR
accTitle: تقسيم العمل بين Media Foundation و WIC
accDescr: Media Foundation يتولّى أخذ الإطار، و WIC يتولّى التحويل إلى صورة والكتابة PNG.
mf1["Media Foundation"] -->|"أخذ الإطار"| fr1["إطار BGRA"]
fr1 -->|"WIC يحوّل إلى صورة"| pg1["ملف 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.
flowchart TB
accTitle: توسيع استخراج إطار واحد إلى استخراج متتالٍ
accDescr: لإطار واحد يكفي تكوين هذه المرّة كما هو، ولعدّة إطارات من الفيديو نفسه غيّر إلى تكرار الـ seek و ReadSample دون إعادة صنع Source Reader.
q3["كم إطاراً نأخذ؟"] -->|"إطار واحد فقط"| as1["تكوين هذه المرّة كما هو"]
q3 -->|"عدّة إطارات"| rp1["تكرار الـ seek والقراءة دون إعادة صنع القارئ"]
rp1 -.-> cost1["انتبه أيضاً إلى تكلفة عدد النسخ"]
الشكل 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 كانت مرّة واحدة فقط.
flowchart TB
accTitle: قراءة فرق Requested و Actual
accDescr: إن انحصر فرق Requested و Actual في نصف فاصل الإطار أو أقلّ فالأمر سليم، وإن ابتعد مئات الميلي ثانية أو أكثر فاشتبِه بأن مقارنة ما قبل وما بعد بعد الـ seek لا تعمل.
dfc["انظر إلى فرق Requested و Actual"] -->|"نصف فاصل الإطار أو أقلّ"| okd["مقارنة ما قبل وما بعد تعمل"]
dfc -->|"مئات ms أو أكثر"| ngd["اشتباه بأن مقارنة ما قبل وما بعد لا تعمل"]
ngd -.-> ck2["تأكّد مثلاً من أن القراءة بعد الـ seek كانت مرّة واحدة فقط"]
الشكل 15: إن تجاوز الفرق نصف فاصل الإطار كثيراً فاشتبِه بتنفيذ مقارنة ما قبل وما بعد.
هدف التحقّق من التشغيل هذه الثلاث.
- رمز الإنهاء 0، وسطر
Saved:يحمل مسار الخرج المحدّد - عند فتح PNG الخارج تظهر مشهد ذلك الزمن بالاتّجاه الصحيح (غير منقلبة رأسيّاً)
- عرض PNG وارتفاعه يطابقان دقّة الفيديو الأصلي، والخلفية غير شفافة (ملء alpha في 4.4 يعمل)
عند الفشل يظهر في الخطأ القياسي Failed. HRESULT = 0x......... اقرأ القيمة بأسلوب 2.1. إن كانت الثواني المحدّدة أكبر من طول الفيديو أو تساويه يصير 0x80070057 (E_INVALIDARG).
flowchart TB
accTitle: تدفّق التحقّق من نتيجة التشغيل
accDescr: عند نجاح التشغيل افحص الأسطر الثلاثة واتّجاه PNG ودقّته وعدم شفافيته، وعند الفشل اقرأ HRESULT في الخطأ القياسي، وإن كانت الثواني أكبر من طول الفيديو أو تساويه يصير E_INVALIDARG.
run1["التشغيل"] -->|"نجاح"| ok3["فحص الأسطر الثلاثة و PNG"]
run1 -->|"فشل"| er2["قراءة HRESULT في الخطأ القياسي"]
er2 -.-> iv1["الثواني أكبر من طول الفيديو أو تساويه تعني E_INVALIDARG"]
الشكل 16: عند النجاح افحص الخرج ونقاط PNG الثلاث، وعند الفشل اقرأ السبب من قيمة HRESULT.
8. الخلاصة
عند استخراج صورة ثابتة عند زمن محدّد من MP4 بـ Media Foundation، النظر إلى SetCurrentPosition و ReadSample وحدهما قليل قليلاً. عمليّاً،
- الـ seek ليس exact
- الإطار أفضل بمقارنة ما قبل وما بعد مع مراقبة timestamp
- قد تنجح
ReadSampleدون عيّنة - امتصّ
strideواتّجاه الصورة ثم احفظ - لا تحسم البايت الرابع في
RGB32على أنه alpha
إن ضبطت هذا المدى قلّ وقوع الحوادث كثيراً.
flowchart TB
accTitle: خمس نقاط تُضبط
accDescr: إن ضُبطت خمس نقاط: الـ seek ليس exact، ومقارنة timestamp قبل وبعد، وفحص غياب العيّنة حتى عند النجاح، وامتصاص stride والاتّجاه، وعدم حسم البايت الرابع في RGB32، قلّ وقوع الحوادث.
k1["الـ seek ليس exact"] --> k2["مقارنة timestamp قبل وبعد"]
k2 --> k3["فحص غياب العيّنة حتى عند النجاح"]
k3 --> k4["امتصاص stride والاتّجاه"]
k4 --> k5["عدم حسم البايت الرابع"]
k5 --> safe2["تنفيذ أقلّ عرضة للحوادث"]
الشكل 17: باكتمال نقاط الضبط الخمس يصير استخراج الصورة الثابتة عند زمن محدّد أقلّ عرضة للحوادث بكثير.
عيّنة هذه المرّة تكوين أدنى ضيّق على أخذ إطار واحد كما ينبغي. جُعل بشكل يمكن جلبه كما هو إلى توليد صور مصغّرة، وحفظ إطار تمثيلي لتصوير مراقبة، وإخراج أثر سجلات فحص.
9. روابط مرجعية
- مجموعة عينات هذه المقالة: media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)
- Microsoft Learn: Using the Source Reader to Process Media Data
- Microsoft Learn:
IMFSourceReader::SetCurrentPosition - Microsoft Learn:
IMFSourceReader::ReadSample - Microsoft Learn:
IMFSourceReader::SetCurrentMediaType - Microsoft Learn:
IMF2DBuffer - Microsoft Learn:
IMF2DBuffer::Lock2D - Microsoft Learn: Uncompressed Video Buffers
- Microsoft Learn: Image Stride
- Microsoft Learn: MF_MT_FRAME_SIZE attribute
- Microsoft Learn: MF_MT_DEFAULT_STRIDE attribute
- Microsoft Learn: Native pixel formats overview (WIC)
- Microsoft Learn: Uncompressed RGB Video Subtypes
10. شيفرة كاملة تُلصَق كما هي في .cpp
الكتلة التالية شيفرة مقدّمتها نقلها كما هي إلى مشروع تطبيق 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,
¤tTimestamp,
&pCurrent);
if (FAILED(hr))
{
SafeRelease(&pCurrent);
break;
}
if ((flags & MF_SOURCE_READERF_ENDOFSTREAM) != 0)
{
SafeRelease(&pCurrent);
if (hasBefore)
{
*ppSample = pBefore;
pBefore = nullptr;
if (pChosenTimestampHns != nullptr)
{
*pChosenTimestampHns = beforeTimestamp;
}
hr = S_OK;
}
else
{
hr = MF_E_END_OF_STREAM;
}
break;
}
if ((flags & MF_SOURCE_READERF_STREAMTICK) != 0)
{
SafeRelease(&pCurrent);
continue;
}
if (pCurrent == nullptr)
{
continue;
}
if (currentTimestamp < targetHns)
{
SafeRelease(&pBefore);
pBefore = pCurrent;
pCurrent = nullptr;
beforeTimestamp = currentTimestamp;
hasBefore = true;
continue;
}
if (hasBefore)
{
diffBefore = targetHns - beforeTimestamp;
diffCurrent = currentTimestamp - targetHns;
if (diffBefore <= diffCurrent)
{
*ppSample = pBefore;
pBefore = nullptr;
if (pChosenTimestampHns != nullptr)
{
*pChosenTimestampHns = beforeTimestamp;
}
SafeRelease(&pCurrent);
}
else
{
*ppSample = pCurrent;
pCurrent = nullptr;
if (pChosenTimestampHns != nullptr)
{
*pChosenTimestampHns = currentTimestamp;
}
}
}
else
{
*ppSample = pCurrent;
pCurrent = nullptr;
if (pChosenTimestampHns != nullptr)
{
*pChosenTimestampHns = currentTimestamp;
}
}
hr = S_OK;
break;
}
SafeRelease(&pBefore);
return hr;
}
HRESULT CopyContiguousBufferToTopDownBgra(
IMFMediaBuffer* pBuffer,
LONG defaultStride,
UINT32 width,
UINT32 height,
std::vector<BYTE>& pixels,
UINT32* pStride)
{
if (pBuffer == nullptr || pStride == nullptr)
{
return E_POINTER;
}
BufferLock lock(pBuffer);
BYTE* pScanLine0 = nullptr;
LONG actualStride = 0;
HRESULT hr = lock.LockBuffer(defaultStride, height, &pScanLine0, &actualStride);
if (FAILED(hr))
{
return hr;
}
if (width > (std::numeric_limits<UINT32>::max() / 4))
{
return E_INVALIDARG;
}
const UINT32 destStride = width * 4;
const LONG actualStrideAbs = (actualStride < 0) ? -actualStride : actualStride;
if (actualStrideAbs < static_cast<LONG>(destStride))
{
return E_UNEXPECTED;
}
pixels.resize(static_cast<size_t>(destStride) * height);
BYTE* pDestRow = pixels.data();
BYTE* pSrcRow = pScanLine0;
for (UINT32 y = 0; y < height; ++y)
{
std::memcpy(pDestRow, pSrcRow, destStride);
// 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 بـ Media Foundation
نرتّب كيف تحرق صورة ونصّاً في كل إطار من فيديو MP4 بـ Media Foundation وتنتج MP4 جديداً، بتقسيم الأدوار بين Source Reader والرسم وتحويل ا...
كيفيّة تحويل YUV إلى RGB في Media Foundation
نرتّب هنا تحويل إطارات YUV إلى RGB في Media Foundation عبر مسارَي التحويل التلقائيّ في Source Reader والتحويل اليدويّ لـ NV12 و YUY2، مع ...
مدخل إلى Media Foundation - فهم الواجهة من منظور COM
نرتّب ما هو Media Foundation مع المصطلحات الأساسيّة لواجهات وسائط Windows مثل COM و HRESULT و IMFSourceReader و MFT، بالترتيب الذي يُضبَط...
Time Travel Debugging ── تسجيل الأخطاء التي لا تتكرّر في التطبيقات طويلة التشغيل وإرجاعها
خطأ يظهر مرّة في الشهر لا يترك في تفريغ الانهيار سوى نتيجته. سجّل التنفيذ وأرجعه بـ WinDbg Time Travel Debugging (TTD): TTD.exe والمخزن ا...
لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows
في Windows لا توجد مصفوفة وسائط؛ يصل إلى CreateProcess سلسلة واحدة ويتولّى الطرف المستقبل تقسيمها. نشرح قواعد CommandLineToArgvW وCRT و.N...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
لأنه حديث عن استخراج صور ثابتة من الفيديو باستخدام Media Foundation وSource Reader وWIC، فهو قريب من موضوعات التنفيذ في تطوير تطبيقات Windows.
الاستشارات التقنية ومراجعة التصميم
إذا أردت ترتيب دقّة seek وصيغة المخزن المؤقّت وstride واتّجاه الصورة قبل التنفيذ، فيمكن البدء بترتيب السياسة ضمن الاستشارة التقنيّة ومراجعة التصميم.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- ماذا نستخدم لاستخراج صورة ثابتة عند زمن محدّد من 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، فيبسط جهة الحفظ ويمنع الانهيار.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.