איך לשלוף still מ-MP4 בזמן נתון עם Media Foundation
· עודכן בתאריך: · Go Komura · Media Foundation, C++, Windows, WIC
היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 15 Mar 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173456)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). איך לשלוף still מ-MP4 בזמן נתון עם Media Foundation. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173456 https://comcomponent.com/he/blog/media-foundation-extract-still-image-from-mp4-at-specific-time/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22173456
- DOI (הגרסה הזו)
- 10.5281/zenodo.22173457
צריך לשלוף מ-MP4 “תמונה אחת בנקודה של 12.3 שניות” — זו דרישה די נפוצה. thumbnail, לוג בדיקה, frame מייצג מווידאו ניטור, תיעוד בלוג של מכשיר.
ב-Media Foundation זה פחות ישר ממה שנראה. נראה כאילו אחרי SetCurrentPosition קוראים ReadSample פעם אחת וזהו, אבל בפועל נכנסים לתמונה key frame, timestamp, stride, כיוון השורות של התמונה, והבייט הרביעי של RGB32. אם הולכים בגישה פשטנית, מקבלים באגים שקטים ומעצבנים: הזמן זז קצת, התמונה הפוכה, ה-PNG יוצא שקוף בלי סיבה.
flowchart TB
accTitle: באגים שיוצאים מגישה פשטנית
accDescr: תרשים שמראה שמימוש של seek ואז קריאה אחת לפני שמירה מערב key frame, timestamp, stride, כיוון שורות, והבייט הרביעי של RGB32, ומקבלים זמן שזז, תמונה הפוכה ו-PNG שקוף.
rough1["seek, קריאה אחת, שמירה"] --> tz1["הזמן זז קצת"]
rough1 --> ud1["התמונה יוצאת הפוכה"]
rough1 --> tp1["ה-PNG יוצא שקוף בלי סיבה"]
איור 1: מימוש שנראה במבט ראשון כאילו זהו, מוביל לבאגים שקטים — זמן שזז, תמונה הפוכה, PNG שקוף.
על התמונה הכללית של Media Foundation כתבתי כבר ב-מבוא ל-Media Foundation - הבנת ה-API מנקודת מבט של COM. הפעם יורדים רמה ומתמקדים רק בשליפת תמונה אחת מ-MP4.
כאן, עם IMFSourceReader, נעבור על איך לשלוף still אחד, הקרוב ביותר לזמן שביקשת, ולשמור אותו כ-PNG, כולל המלכודות שקל ליפול בהן בפרודקשן. בסוף יש קוד שלם בקובץ אחד, שקל להדביק ל-.cpp של פרויקט C++ Console App ב-Visual Studio. אין שברי קוד לאורך המאמר — מספיק לקחת את הבלוק האחרון.
הקוד במאמר הזה מפורסם גם כסט דוגמה שלם (C++ console app בקובץ אחד) ב-GitHub.
media-foundation-extract-still-image-from-mp4-at-specific-time - komurasoft-blog-samples (GitHub)
1. קודם המסקנה
בקצרה:
- לשלוף תמונה אחת מ-MP4, הפעם
Source Readerהוא entry point נוח יותר מ-Media Session IMFSourceReader::SetCurrentPositionלא מבטיח exact seek. בדרך כלל הוא נוחת קצת לפני היעד, בעיקר לכיוון key frame, אז אחרי זה ממשיכים עםReadSampleומשווים לפני ואחרי הזמן שביקשתReadSampleיכול להצליח ועדיין להחזירpSample == nullptr. בודקים לא רקHRESULT, גםflagsו-pSample- כשמכוונים את output media type ל-
MFVideoFormat_RGB32, השמירה נוחה יותר - אבל הבייט הרביעי של
RGB32הוא לא בהכרח alpha. כתיבה ישירה ל-PNG יכולה לתת תמונה שקופה. לפני השמירה שמים0xFFועושים opaque - אם מתעלמים מ-
strideלכל שורה ומכיוון top-down / bottom-up, התמונה נשברת. את ה-sample שהתקבל אורזים קודם ל-BGRA רציף top-down, ורק אז מעבירים ל-PNG
כלומר, seek -> קריאה אחת -> שמירה זה פשטני מדי. seek -> השוואה לפי timestamp -> copy תוך התחשבות ב-stride -> שמירת PNG יציב בהרבה.
flowchart TB
accTitle: הזרימה הפשטנית מול הזרימה היציבה
accDescr: תרשים שמראה ש-seek ואז קריאה אחת לפני שמירה זה פשטני, ואילו אחרי seek משווים לפי timestamp, מעתיקים תוך התחשבות ב-stride ואז שומרים PNG — יציב בהרבה.
sk1["seek"] --> cmp1["השוואה לפני ואחרי לפי timestamp"]
cmp1 --> cp1["copy תוך התחשבות ב-stride"]
cp1 --> sv1["שמירת PNG"]
sk1 -.->|"קריאה אחת ושמירה — פשטני"| ng1["גורם לסטייה ולעיוות"]
איור 2: לא שומרים מיד אחרי ה-seek. מוסיפים השוואה ו-copy שמתחשב ב-stride, והתוצאה יציבה.
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 15, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
2. הנחות של המאמר
2.1. קהל יעד וידע מקדים
- מניחים שכבר קראתם COM API ב-Windows מ-C++. מספיק להכיר את הזרימה: אתחול עם
CoInitializeEx, ושחרור מצביע interface עםRelease - את ערך ההחזרה
HRESULTבודקים עם המאקרואיםSUCCEEDED/FAILED. בכשל קוראים את הערך ההקסדצימלי כמו שהוא. ערך שמתחיל ב-0x8007מגיע משגיאת Win32, ו-16 הסיביות הנמוכות הן קוד השגיאה של Win32 (0x80070057זה אותו ערך כמוE_INVALIDARG). שגיאות ספציפיות ל-Media Foundation שמתחילות ב-MF_E_מוגדרות ב-mferror.h - לא צריך היכרות מוקדמת עם Media Foundation עצמו. התמונה הכללית מזווית COM נמצאת ב-מבוא ל-Media Foundation - הבנת ה-API מנקודת מבט של COM
2.2. סביבת פיתוח
| פריט | ההנחה הפעם |
|---|---|
| OS | Windows 10 / Windows 11 |
| IDE | Visual Studio 2022 (workload “Desktop development with C++”) |
| SDK | Windows SDK שמגיע עם Visual Studio (כולל headers וספריות של Media Foundation ו-WIC) |
| פרויקט | template “Console App” של C++ |
| פלטפורמה | x64 |
| ספריות נוספות | אין. קישור כמו mfplat.lib נעשה עם #pragma comment(lib, ...) בתוך הקוד |
איך מדביקים, ומה לעשות עם precompiled headers, מרוכז בפרק “7. הערות build והרצה”.
2.3. הנחות קלט ופלט
ההנחות הפעם:
- הקלט הוא קובץ MP4 מקומי
- הפלט הרצוי הוא still אחד
- לא “בדיוק בזמן שביקשת”, אלא “ה-frame הקרוב ביותר לזמן שביקשת”
- המימוש הוא
IMFSourceReaderבמצב synchronous - פורמט השמירה הוא PNG דרך WIC
- בלי ספריות חיצוניות — הכול על Windows API סטנדרטי
- מניחים MP4 רגיל שהרזולוציה לא משתנה באמצע
אם צריך גם playback, סנכרון אודיו, seek bar וחיבור ל-UI, יש עיצוב אחר. למטרה של לשלוף frame אחד הזרימה הזו ברורה למדי.
flowchart TB
accTitle: מבנה ההנחות במאמר
accDescr: תרשים שמראה ש-MP4 מקומי נכנס ל-IMFSourceReader synchronous, ששולף את ה-frame הקרוב ביותר לזמן שביקשת ושומר PNG עם WIC, בלי ספריות חיצוניות.
mp1["MP4 מקומי"] --> rd1["IMFSourceReader במצב synchronous"]
rd1 --> nf1["ה-frame הקרוב ביותר לזמן שביקשת"]
nf1 --> png1["שמירת PNG עם WIC"]
rd1 -.-> std1["הכול על Windows API סטנדרטי"]
איור 3: מהקלט עד השמירה רצים על Source Reader synchronous, בלי ספריות חיצוניות.
3. טבלת סיכום שכדאי לראות קודם
3.1. זרימת העיבוד
| מה עושים | ה-API | התפקיד |
|---|---|---|
| פתיחת MP4 | MFCreateSourceReaderFromURL |
בניית media source מקובץ |
| בחירת וידאו בלבד | SetStreamSelection |
לא לקרוא אודיו |
| המרה ל-RGB32 | SetCurrentMediaType + MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING |
לקבל uncompressed frame שנוח לשמור |
| מעבר לזמן שביקשת | SetCurrentPosition |
seek ביחידות של 100ns |
| קריאת frame | ReadSample |
לקבל sample מפוענח אחד בכל פעם |
| השוואת לפני / אחרי | timestamp של ה-sample | לבחור את ה-frame הקרוב ביותר לזמן שביקשת |
| שמירה כ-PNG | WIC | כתיבה לקובץ תמונה |
3.2. כלל הבחירה הפעם
גם כשאומרים “ה-still של הזמן שביקשת”, וידאו הוא לא רצף רציף אלא frames בדידים. במימוש נוח לקבוע מראש לפי איזה כלל בוחרים את התמונה האחת.
הכלל הפעם:
- אחרי ה-seek ממשיכים עם
ReadSample - שומרים את ה-sample האחרון ש-
timestamp < target - כשמגיע ה-sample הראשון עם
timestamp >= target, משווים את ההפרש של ה-sample הקודם ושל הנוכחי - בוחרים את הקרוב יותר ליעד
כך מקבלים את ה-frame הקרוב ביותר ליעד, לא “הראשון אחרי היעד”.
flowchart TB
accTitle: כלל הבחירה ל-frame הקרוב ביותר
accDescr: תרשים שמראה שאחרי seek ממשיכים עם ReadSample, שומרים את ה-sample האחרון שלפני היעד, וכשמגיע הראשון שאחריו משווים את שני ההפרשים ובוחרים את הקרוב יותר.
adv1["המשך עם ReadSample אחרי seek"] --> bf1["שומרים את ה-sample האחרון שלפני היעד"]
bf1 --> af1["מגיע ה-sample הראשון שאחרי היעד"]
af1 --> df1["משווים את שני ההפרשים מהיעד"]
df1 --> pk1["בוחרים את הקרוב יותר"]
איור 4: משווים שני מועמדים, לפני ואחרי, ורק אז בוחרים — כך מקבלים את הקרוב ביותר, לא רק את הראשון אחרי היעד.
3.3. תמונת הזרימה
מההתחלה עד הסוף, בערך: input.mp4 -> בניית Source Reader -> בקשת RGB32 -> seek -> חזרה על ReadSample -> השוואה לפני ואחרי היעד -> אריזה מחדש ל-top-down BGRA -> שמירת PNG עם WIC.
זה נראה פשוט, אבל בארבעה מקומות יושבות מלכודות קטנות — דיוק ה-seek, sample שהוא null, stride, והבייט הרביעי. אם לא נתקעים באחת מהן, המימוש עצמו נשאר פשוט.
flowchart TB
accTitle: ארבע המלכודות בזרימה
accDescr: תרשים שמראה שהזרימה נראית פשוטה אבל יש ארבע מלכודות קטנות - דיוק seek, sample שהוא null, stride, והבייט הרביעי של RGB32 - ואם לא נתקעים בהן המימוש נשאר פשוט.
lk1["זרימה שנראית פשוטה"] --> t1["דיוק ה-seek"]
lk1 --> t2["sample שהוא null"]
lk1 --> t3["stride וכיוון"]
t3 -.-> t4["הטיפול בבייט הרביעי"]
t1 --> okf["בלי להיתקע — המימוש נשאר פשוט"]
t2 --> okf
t3 --> okf
איור 5: בזרימה שנראית פשוטה יש ארבע מלכודות. שמים לב רק אליהן, והקוד עצמו נשאר פשוט.
4. מלכודות שכדאי לסגור מראש
לפני שממשיכים, מילון קצר למונחים בפרק הזה.
| מונח | משמעות |
|---|---|
| key frame | frame שאפשר לפענח לבד, בלי להסתמך על frames אחרים. ב-H.264 זה למשל תמונת IDR |
| GOP (Group of Pictures) | קבוצת ה-frames מ-key frame אחד עד רגע לפני ה-key frame הבא. ככל שה-GOP ארוך יותר, המרווח בין key frames גדול יותר, ואחרי seek קל יותר לנחות רחוק מהזמן שביקשת |
| stride | כמה bytes תופסת שורה אחת ב-buffer של התמונה. לא בהכרח שווה ל-width × bytesPerPixel — לפעמים יש padding בסוף השורה |
| top-down / bottom-up | האם תחילת ה-buffer היא השורה העליונה או התחתונה. bottom-up מיוצג עם stride שלילי |
MF_SOURCE_READERF_STREAMTICK |
אחד מה-flags ש-ReadSample מחזיר. מציין פער (gap) ב-stream. בקריאה שבה ה-flag הזה דולק לא מתקבל frame, וקוראים שוב |
4.1. SetCurrentPosition הוא לא exact seek
כפי שכתוב גם ב-Microsoft Learn על IMFSourceReader::SetCurrentPosition, זה לא מבטיח exact seeking. בווידאו הוא בדרך כלל נוחת קצת לפני היעד, בעיקר לכיוון key frame. בנוסף, מניחים שאחרי זה ממשיכים עם ReadSample עד לנקודה שביקשת.
לכן מימוש מהסוג הבא מסוכן למדי:
SetCurrentPosition(target)ReadSampleפעם אחת- שומרים את אותו frame
על וידאו עם GOP ארוך זה זז. אם המרווח בין key frames הוא 2 שניות, ה-frame שנשמר יכול להיות עד כמעט 2 שניות לפני הזמן שביקשת.
flowchart TB
accTitle: למה קריאה אחת אחרי seek זזה
accDescr: תרשים שמראה ש-SetCurrentPosition נוחת קצת לפני היעד, בעיקר לכיוון key frame, ולכן קריאה אחת מיד אחר כך יכולה לשמור frame רחוק מהזמן שביקשת על וידאו עם GOP ארוך.
sp1["SetCurrentPosition (target)"] --> kf1["נחיתה קצת לפני, לכיוון key frame"]
kf1 --> one1["ReadSample פעם אחת בלבד"]
one1 --> ng2["שומרים frame מוקדם מדי"]
ng2 -.-> gp1["GOP ארוך יותר — סטייה גדולה יותר"]
איור 6: ה-seek נוחת לכיוון key frame, אז קריאה אחת יכולה לשמור תמונה מוקדמת באורך ה-GOP.
4.2. ReadSample יכול להצליח ועדיין להחזיר pSample == nullptr
ReadSample יכול להחזיר S_OK וגם ppSample שהוא NULL. בסוף ה-stream חוזר flag כמו MF_SOURCE_READERF_ENDOFSTREAM, ובפער ב-stream — MF_SOURCE_READERF_STREAMTICK.
מסוכן לבדוק רק HRESULT ולגשת ישר ל-pSample. בטוח יותר לבדוק שלושה דברים יחד: HRESULT, flags ו-pSample.
flowchart TB
accTitle: בדיקה משולשת של תוצאת ReadSample
accDescr: תרשים שמראה ש-ReadSample יכול להחזיר S_OK עם sample שהוא NULL, ולכן בודקים יחד HRESULT, flags ו-pSample, ומטפלים ב-flags של סוף stream או gap.
rs1["התוצאה של ReadSample"] --> h1["בודקים HRESULT"]
rs1 --> f1["בודקים flags"]
rs1 --> s1["בודקים pSample"]
f1 -.-> gap1["יכול להיות flag של סוף stream או gap"]
s1 -.-> nl1["גם עם S_OK יכול להיות NULL"]
איור 7: לא מסתפקים בקוד הצלחה — בודקים יחד HRESULT, flags ו-pSample.
4.3. אם מתעלמים מ-stride ומכיוון השורות, התמונה נשברת
buffer של תמונה לא תמיד רציף לפי width * bytesPerPixel. לפעמים יש padding בסוף שורה, ופורמטי RGB יכולים להיות bottom-up. גם ב-Image Stride וגם ב-Uncompressed Video Buffers של Microsoft Learn זה כתוב במפורש.
שתי נקודות שחשובות במיוחד:
IMF2DBuffer::Lock2Dמחזיר את המצביע לתחילת scan line 0 ואת ה-stride האמיתי- בתמונה bottom-up ה-stride יכול להיות שלילי
הפעם מאמצים את הגישה של ה-helper ב-Microsoft Learn, ואורזים מחדש ל-buffer BGRA רציף top-down לפני שמעבירים ל-PNG. כשסוגרים את זה מראש, צד השמירה נהיה פשוט בהרבה.
flowchart TB
accTitle: טיפול ב-stride ובכיוון
accDescr: תרשים שמראה שמקבלים עם Lock2D של IMF2DBuffer את המצביע לתחילת scan line 0 ואת ה-stride האמיתי, כולל bottom-up עם stride שלילי, ואורזים מחדש ל-buffer BGRA רציף top-down לפני שמעבירים ל-PNG.
l2d["מקבלים עם Lock2D"] --> sl0["מצביע לתחילת scan line 0"]
l2d --> ast1["ה-stride האמיתי"]
ast1 -.-> neg1["ב-bottom-up יכול להיות שלילי"]
sl0 --> pack1["אורזים מחדש ל-BGRA רציף top-down"]
ast1 --> pack1
pack1 --> sim1["צד השמירה נהיה פשוט"]
איור 8: עם Lock2D לוקחים את ה-stride האמיתי ואת הכיוון, אורזים ל-BGRA רציף top-down, ורק אז שומרים.
4.4. לא להניח שהבייט הרביעי של MFVideoFormat_RGB32 הוא alpha
בניגוד למה שהשם מרמז, MFVideoFormat_RGB32 הוא לא “RGBA נקי” שאפשר להעביר ישר ל-PNG. ב-32-bit RGB של Windows, bytes 0, 1, 2 הם B, G, R, ו-byte 3 יכול להיות alpha או ignore. הנקודה החשובה: זה לא ARGB32.
אם מניחים שזה GUID_WICPixelFormat32bppBGRA ושומרים כמו שהוא, הבייט הרביעי יכול להיות 0 והתמונה יוצאת שקופה בלי סיבה. הפעם המדיניות היא למלא alpha ב-0xFF לפני השמירה, כדי שהתמונה תהיה fully opaque.
flowchart TB
accTitle: הטיפול בבייט הרביעי של RGB32
accDescr: תרשים שמראה שב-MFVideoFormat_RGB32 bytes 0 עד 2 הם B, G, R והבייט הרביעי הוא לא בהכרח alpha, ולכן כתיבה ישירה ל-PNG יכולה לצאת שקופה, וממלאים 0xFF לפני השמירה כדי לקבל opaque.
rgb1["frame בפורמט MFVideoFormat_RGB32"] --> b4["הבייט הרביעי הוא לא בהכרח alpha"]
b4 -->|"שמירה ישירה"| tp2["יכול לצאת PNG שקוף"]
b4 -->|"ממלאים 0xFF ואז שומרים"| op1["PNG fully opaque"]
איור 9: לא מקבעים מראש את הבייט הרביעי — ממלאים 0xFF לפני השמירה וסוגרים opaque.
5. זרימת המימוש
5.1. בניית Source Reader במצב synchronous
הפעם מספיק תמונה אחת, אז בוחרים ReadSample synchronous במקום async callback. במצב synchronous, ReadSample חוסם עד ה-sample הבא, אבל לשליפת still בודד המימוש נשאר פשוט למדי.
ביצירת ה-Reader עושים ארבעה דברים:
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE- כיבוי כל ה-streams קודם
- הפעלת
MF_SOURCE_READER_FIRST_VIDEO_STREAMבלבד - הגדרת output type ל-
MFMediaType_Video/MFVideoFormat_RGB32
אחרי זה נוח לכתוב את המשך הקוד בהנחה ש”מגיע frame בפורמט RGB32”.
flowchart TB
accTitle: ארבעת השלבים בבניית ה-Reader
accDescr: תרשים שמראה שביצירת ה-Reader מפעילים עיבוד וידאו, מכבים קודם את כל ה-streams, מדליקים רק את video stream הראשון, ומגדירים את output type ל-RGB32.
c1["מדליקים עיבוד וידאו"] --> c2["מכבים את כל ה-streams"]
c2 --> c3["מדליקים רק את video stream הראשון"]
c3 --> c4["מגדירים output type ל-RGB32"]
c4 -.-> rdy1["המשך הקוד יכול להניח RGB32"]
איור 10: אחרי ארבעת השלבים בבניית ה-Reader, המשך הקוד מקבל רק frames ב-RGB32.
5.2. אחרי seek ממשיכים לפי timestamp
אחרי SetCurrentPosition לא שומרים מיד. תוך כדי קריאת sample עם ReadSample, משווים את ה-frame האחרון שלפני היעד עם הראשון שאחריו.
הצעד הקטן הזה סופג חלק גדול מחוסר הדיוק של ה-seek.
sequenceDiagram
accTitle: רצף ההשוואה אחרי seek
accDescr: תרשים שמראה שהאפליקציה עושה seek עם SetCurrentPosition, קוראת שוב ושוב עם ReadSample תוך בדיקת timestamp, וברגע שחוצים את היעד משווים את ה-samples שלפני ואחרי ובוחרים את הקרוב יותר.
participant A as האפליקציה
participant R as Source Reader
A->>R: SetCurrentPosition (target)
loop עד שחוצים את target
A->>R: ReadSample
R-->>A: sample ו-timestamp
end
A->>A: משווים את ה-samples שלפני ואחרי ובוחרים את הקרוב יותר
איור 11: לא שומרים מיד אחרי ה-seek. ממשיכים לקרוא עד שחוצים את היעד, ואז משווים.
5.3. מ-sample ל-BGRA top-down
לא כותבים את ה-sample שהתקבל ישר ל-PNG. קודם אורזים אותו ל-buffer BGRA top-down.
- הופכים ל-buffer אחד עם
ConvertToContiguousBuffer - מקבלים scan line 0 ואת ה-stride האמיתי עם ה-helper
BufferLock - מעתיקים שורה-שורה ל-buffer top-down
- שמים alpha ל-
0xFF
אחרי זה צד השמירה יכול להתייחס לזה כאל “תמונת 32bpp BGRA רגילה”.
flowchart TB
accTitle: שלבי האריזה מ-sample ל-BGRA
accDescr: תרשים שמראה שהופכים ל-buffer אחד עם ConvertToContiguousBuffer, מקבלים עם BufferLock את scan line 0 ואת ה-stride האמיתי, מעתיקים שורה-שורה ל-buffer top-down ושמים alpha ל-0xFF.
cv1["ConvertToContiguousBuffer"] --> bl1["scan line 0 ו-stride עם BufferLock"]
bl1 --> rc1["copy שורה-שורה ל-top-down"]
rc1 --> al1["alpha ל-0xFF"]
al1 --> out1["תמונת 32bpp BGRA רגילה"]
איור 12: אחרי ארבעת שלבי האריזה, צד השמירה יכול להתייחס לזה כאל תמונת 32bpp BGRA רגילה.
5.4. שמירת PNG עוברת ל-WIC
השמירה נעשית עם IWICBitmapEncoder / IWICBitmapFrameEncode של WIC. Media Foundation שולף את ה-frame, WIC הופך אותו לתמונה — זו חלוקת העבודה. הכול על Windows API סטנדרטי.
flowchart LR
accTitle: חלוקת העבודה בין Media Foundation ל-WIC
accDescr: תרשים שמראה ש-Media Foundation אחראי לשליפת ה-frame ו-WIC אחראי להפוך אותו לתמונה ולכתוב PNG.
mf1["Media Foundation"] -->|"שולף את ה-frame"| fr1["frame BGRA"]
fr1 -->|"WIC הופך לתמונה"| pg1["קובץ PNG"]
איור 13: שליפת frame ב-Media Foundation, הפיכה לתמונה ב-WIC — הכול על API סטנדרטי בלבד.
6. checklist למימוש
| פריט | מה בודקים | מה קורה אם מפספסים |
|---|---|---|
| דיוק ה-seek | לא לבחור לפי קריאה אחת מיד אחרי SetCurrentPosition |
שומרים frame רחוק מאוד לפני הזמן שביקשת |
| NULL של ה-sample | בודקים HRESULT, flags ו-pSample יחד |
null dereference בסוף ה-stream או ב-gap |
| stride | לוקחים את ה-stride האמיתי ואת כיוון השורות | התמונה נשברת, יוצאת הפוכה |
| הבייט הרביעי של RGB32 | שמים alpha ל-0xFF |
PNG שקוף |
| טווח הזמן | שומרים על 0 <= target < duration |
התנהגות לא צפויה ליד הסוף |
| שליפה רצופה | חוזרים על seek בלי לבנות מחדש את ה-Reader | איטיות מיותרת |
| מספר ה-copies | בעיבוד כבד מתחשבים בעלות של ConvertToContiguousBuffer |
בזבוז CPU ו-memory bandwidth |
| שינוי פורמט | וידאו מיוחד שהרזולוציה משתנה באמצע דורש עיצוב נפרד | הנחות width/height נשברות |
מתוך אלה, רק שתי השורות “שליפה רצופה” ו”מספר ה-copies” רלוונטיות לא לשליפת תמונה אחת, אלא כשמרחיבים לעשרות תמונות מאותו וידאו. הדוגמה הפעם נגמרת בשליפת תמונה אחת, אז כששולפים כמה תמונות עוברים מבנייה מחדש של Source Reader לחזרה על SetCurrentPosition ו-ReadSample. המפה הכללית של Media Foundation, כולל חלופות ל-Source Reader (כמו Media Session), נמצאת ב-מבוא ל-Media Foundation - הבנת ה-API מנקודת מבט של COM.
flowchart TB
accTitle: הרחבה משליפת תמונה אחת לשליפה רצופה
accDescr: תרשים שמראה שאם צריך תמונה אחת ההרכב הנוכחי מספיק, ולשליפת כמה תמונות מאותו וידאו עוברים לחזרה על seek ו-ReadSample בלי לבנות מחדש את ה-Reader.
q3["כמה תמונות שולפים"] -->|"אחת בלבד"| as1["ההרכב הנוכחי מספיק"]
q3 -->|"כמה תמונות"| rp1["חוזרים על seek וקריאה בלי לבנות מחדש את ה-Reader"]
rp1 -.-> cost1["גם עלות ה-copy חשובה"]
איור 14: כשמרחיבים לכמה תמונות, חוזרים על seek וקריאה בלי לבנות מחדש את ה-Reader.
7. הערות build והרצה
הקוד בסוף המאמר בנוי כך שקל להוסיף כקובץ .cpp אחד לפרויקט C++ Console App ב-Visual Studio. הנחות הסביבה עצמה מרוכזות בפרק 2.2.
7.1. הערות build
כמה נקודות שכדאי לדעת מראש:
- יש
#pragma comment(lib, ...), אז בדרך כלל לא צריך הגדרות linker נוספות - משתמשים ב-
wmain, כך ש-command-line arguments מטופלים כ-Unicode לכל האורך - כדי שיהיה קל להדביק גם אם ב-template ברירת המחדל של Console App יש
pch.hאוstdafx.h, בראש הקוד יש__has_includeשמזהה אותם - ואם בכל זאת הפרויקט כופה precompiled header משלו, מספיק להגדיר ל-
.cppהזה בלבד “Not Using Precompiled Headers” כדי שזה יעבור - מומלץ להריץ ב-x64
7.2. איך מריצים, ומה יוצא בהצלחה
השימוש: ExtractFrameFromMp4.exe <input.mp4> <seconds> <output.png>. לדוגמה: ExtractFrameFromMp4.exe C:\work\input.mp4 12.345 C:\work\frame.png.
בהצלחה, בסוף wmain מודפסות שלוש השורות הבאות ל-stdout:
Saved: C:\work\frame.png
Requested: 12.345 sec
Actual: (זמן התצוגה של ה-frame שנבחר) sec
Requested הוא בדיוק מספר השניות שהועבר בארגומנט, ו-Actual הוא ה-timestamp בפועל של ה-frame שנבחר. לפי הכלל בסעיף 3.2 בוחרים את הקרוב יותר מבין ה-frames שלפני ואחרי, אז ההפרש נשאר בערך בתוך חצי ממרווח ה-frames (ב-29.97fps, בערך 17ms כגבול עליון). אם ההפרש עולה על כמה מאות ms, כדאי לחשוד שקוראים רק פעם אחת אחרי ה-seek, כלומר שההשוואה מפרק 4.1 לא עובדת.
flowchart TB
accTitle: איך קוראים את ההפרש בין Requested ל-Actual
accDescr: תרשים שמראה שאם ההפרש בין Requested ל-Actual בתוך חצי ממרווח ה-frames זה תקין, ואם הוא כמה מאות ms או יותר כדאי לחשוד שההשוואה שלפני ואחרי לא עובדת.
dfc["בודקים את ההפרש בין Requested ל-Actual"] -->|"בתוך חצי ממרווח ה-frames"| okd["ההשוואה עובדת"]
dfc -->|"כמה מאות ms או יותר"| ngd["חשד שההשוואה לא עובדת"]
ngd -.-> ck2["לבדוק אם קוראים רק פעם אחת אחרי ה-seek"]
איור 15: אם ההפרש גדול בהרבה מחצי ממרווח ה-frames, כדאי לחשוד במימוש ההשוואה שלפני ואחרי.
מטרות בדיקת ההרצה הן שלוש:
- exit code הוא 0, ובשורת
Saved:מופיע נתיב הפלט שהוגדר - כשפותחים את ה-PNG שנוצר, הסצנה של אותו זמן מוצגת בכיוון הנכון (לא הפוכה)
- width ו-height של ה-PNG תואמים לרזולוציית הווידאו המקורי, והרקע לא שקוף (כלומר מילוי ה-alpha מפרק 4.4 עבד)
בכישלון מודפס ל-stderr Failed. HRESULT = 0x......... את הערך קוראים לפי השיטה מפרק 2.1. אם מספר השניות שהוזן גדול או שווה לאורך הווידאו, מתקבל 0x80070057 (E_INVALIDARG).
flowchart TB
accTitle: זרימת בדיקת תוצאת ההרצה
accDescr: תרשים שמראה שאם ההרצה הצליחה בודקים את שלוש שורות הפלט ואת התמונה, ואם נכשלה קוראים HRESULT מ-stderr, ומספר שניות שגדול או שווה לאורך הווידאו נותן E_INVALIDARG.
run1["הרצה"] -->|"הצלחה"| ok3["בודקים שלוש שורות פלט ואת ה-PNG"]
run1 -->|"כישלון"| er2["קוראים HRESULT מ-stderr"]
er2 -.-> iv1["שניות גדולות או שוות לאורך הווידאו נותנות E_INVALIDARG"]
איור 16: בהצלחה בודקים את שלושת הפרטים בפלט וב-PNG. בכישלון קוראים את הסיבה מערך ה-HRESULT.
8. סיכום
כששולפים still מ-MP4 בזמן נתון עם Media Foundation, לא מספיק להסתכל רק על SetCurrentPosition ו-ReadSample. בפועל:
- ה-seek הוא לא exact
- כדאי לבדוק timestamp של ה-frame ולהשוות לפני ואחרי
- גם כש-
ReadSampleמצליח, יכול להיות שאין sample - מטפלים ב-
strideובכיוון התמונה לפני השמירה - לא מניחים שהבייט הרביעי של
RGB32הוא alpha
אם סוגרים את הנקודות האלה, הסיכוי לבאגים יורד משמעותית.
flowchart TB
accTitle: חמש הנקודות שכדאי לסגור
accDescr: תרשים שמראה שחמש הנקודות - ה-seek הוא לא exact, השוואת timestamp לפני ואחרי, בדיקת sample חסר גם בהצלחה, טיפול ב-stride ובכיוון, ואי-הנחה מראש על הבייט הרביעי - מורידות באגים.
k1["ה-seek הוא לא exact"] --> k2["השוואת timestamp לפני ואחרי"]
k2 --> k3["בודקים sample חסר גם בהצלחה"]
k3 --> k4["מטפלים ב-stride ובכיוון"]
k4 --> k5["לא מניחים מראש על הבייט הרביעי"]
k5 --> safe2["מימוש עם פחות באגים"]
איור 17: כשחמש הנקודות מכוסות יחד, שליפת still בזמן נתון הרבה פחות נוטה לבאגים.
הדוגמה הפעם היא הרכב מינימלי שממוקד בשליפה תקינה של תמונה אחת. הצורה מתאימה ישירות לשימושים כמו יצירת thumbnail, שמירת frame מייצג מווידאו ניטור, ותיעוד ללוג בדיקה.
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
הבלוק הבא, בשלמותו, הוא הקוד שמיועד להעברה ישירה לפרויקט C++ Console App ב-Visual Studio. סדר ה-command-line arguments הוא input.mp4, seconds, output.png. ההרכב הוא קובץ אחד שלם, כך שקל להדביק אותו לפרויקט.
זה בלוק ארוך אחד, אז קודם טבלת התאמה בין הפונקציות לבין המקומות במאמר. גם בקריאה וגם בחקירה כשמשהו לא עובד, נוח להתחיל מהטבלה הזו.
| פונקציה / מחלקה בקוד | המקום במאמר | תפקיד, והמלכודת שהיא מטפלת בה |
|---|---|---|
MediaFoundationScope |
5.1 | מרכזת אתחול וניקוי של CoInitializeEx ו-MFStartup |
CreateConfiguredSourceReader |
5.1 | בונה את ה-Reader, בוחרת רק video stream, ומבקשת MFVideoFormat_RGB32 |
GetPresentationDuration |
6. (טווח הזמן) | לוקחת את אורך הווידאו, ובודקת שמתקיים 0 <= target < duration |
SeekSourceReader |
4.1 / 5.2 | עושה seek עם SetCurrentPosition. לבד זה לא exact |
ReadNearestVideoSample |
3.2 / 4.1 / 4.2 / 5.2 | משווה לפני ואחרי היעד ובוחרת frame אחד. גם מטפלת כאן ב-flags וב-null של pSample |
GetDefaultStride |
4.3 | מחשבת ומשלימה stride כשאין MF_MT_DEFAULT_STRIDE |
BufferLock |
4.3 / 5.3 | מקבלת עם IMF2DBuffer::Lock2D את scan line 0 ואת ה-stride האמיתי |
CopyContiguousBufferToTopDownBgra |
4.3 / 4.4 / 5.3 | מעתיקה שורה-שורה ל-buffer top-down, וממלאת את הבייט הרביעי ב-0xFF |
CopySampleToTopDownBgra |
5.3 | מוציאה frame size ו-stride וקוראת ל-copy שלעיל |
SaveBgraToPng |
5.4 | כותבת עם WIC תמונת 32bpp BGRA כ-PNG |
ExtractFrameFromMp4ToPng |
5. כולו | entry point שקורא לפונקציות שלעיל בסדר |
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 הוא לא בהכרח alpha,
// אז לפני שמירת PNG מקבעים אותו ל-opaque.
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;
}
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
מבוא ל-Media Foundation: להבין את ה-API דרך COM
מה זה Media Foundation, דרך המונחים הבסיסיים של Windows media API — COM, HRESULT, IMFSourceReader ו-MFT — בסדר שכדאי לתפוס אותם קודם.
Time Travel Debugging — להקליט ולהריץ אחורה באגים שלא משתחררים באפליקציות ארוכות-ריצה
באג פעם בחודש משאיר ב-crash dump רק את התוצאה. מקליטים ומריצים אחורה את הביצוע עם WinDbg Time Travel Debugging (TTD): TTD.exe, ring buffe...
למה arguments נשברים — כללי command-line arguments ב-Windows
Windows מעביר ל-CreateProcess מחרוזת אחת שהמקבל מפצל. מכסה את כללי CommandLineToArgvW, CRT ו-.NET, ArgumentList, ובניה ב-C++.
מה נשאר אחרי שה-parent מת — מחזיקים child processes ב-Job Object
למה SDK helpers שורדים UI שנהרג ומחזיקים את המצלמה או את ה-COM port? מתכננים משך חיים של child process עם Job Objects, KillOnJobClose ו-c...
Win32 Thread Pool API — מקביליות בלי CreateThread, דרך CreateThreadpoolWork
מפזרים קריאות CreateThread בכל הקוד ה-native? המאמר מסביר את ה-Win32 Thread Pool API שעוצב מחדש ב-Vista: ארבעת האובייקטים work, timer, wa...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
הנושא הוא חילוץ still מווידאו עם Media Foundation, Source Reader ו-WIC, ולכן הוא קרוב למימוש בפיתוח אפליקציות Windows.
ייעוץ טכני וסקירת תכנון
אם לפני הקוד רוצים לסגור דיוק seek, פורמט buffer, stride וכיוון התמונה, אפשר להתחיל מייעוץ טכני ו-design review.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- במה משתמשים כדי לשלוף still מ-MP4 בזמן נתון?
- אם צריך תמונה אחת, IMFSourceReader הוא entry point נוח יותר מ-Media Session. פותחים את הקובץ עם MFCreateSourceReaderFromURL, בוחרים רק וידאו עם SetStreamSelection, מבקשים MFVideoFormat_RGB32, עושים seek עם SetCurrentPosition, קוראים frame עם ReadSample, ושומרים PNG עם WIC. הכול על Windows API סטנדרטי, בלי ספריות חיצוניות.
- האם SetCurrentPosition מזיז בדיוק לזמן שביקשת?
- לא. IMFSourceReader::SetCurrentPosition לא מבטיח exact seeking. בווידאו הוא בדרך כלל נוחת קצת לפני היעד, בעיקר לכיוון key frame. אחרי ה-seek ממשיכים עם ReadSample, בודקים timestamp, משווים את ה-sample האחרון שלפני היעד עם הראשון שאחריו, ובוחרים את הקרוב יותר. מימוש שקורא פעם אחת אחרי seek ושומר ייסחף בקלות על וידאו עם GOP ארוך.
- למה ה-PNG שנשמר יוצא שקוף?
- כי הבייט הרביעי של MFVideoFormat_RGB32 הוא לא בהכרח alpha. ב-32-bit RGB של Windows, bytes 0, 1, 2 הם B, G, R, ו-byte 3 יכול להיות alpha או ignore (זה לא ARGB32). כתיבה ישירה ל-PNG יכולה לתת תמונה שקופה, אז לפני השמירה שמים 0xFF בבייט הרביעי ועושים את התמונה opaque.
- מה גורם לתמונה לצאת שבורה או הפוכה?
- stride וכיוון השורות. buffer של תמונה לא תמיד רציף לפי width × bytesPerPixel — לפעמים יש padding בסוף שורה, ופורמטי RGB יכולים להיות bottom-up (stride שלילי). עם IMF2DBuffer::Lock2D מקבלים מצביע לתחילת scan line 0 ואת ה-stride האמיתי, ואורזים מחדש ל-buffer BGRA רציף top-down לפני ה-PNG. צד השמירה נהיה פשוט, והעיוות נמנע.