Как наложить изображение и текст на кадры MP4 в Media Foundation
· Обновлено: · Го Комура · Media Foundation, C++, Разработка Windows, GDI+, Direct2D, DirectWrite, H.264
История изменений (1 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619720)
Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.
Го Комура (2026). Как наложить изображение и текст на кадры MP4 в Media Foundation. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619720 https://comcomponent.com/ru/blog/2026/03/16/009-media-foundation-overlay-image-text-on-mp4-frames/
- DOI (последняя версия)
- 10.5281/zenodo.21619720
- DOI (эта версия)
- 10.5281/zenodo.21619721
Водяной знак логотипа, результат проверки, номер оборудования, имя оператора, метка времени. Требование прожечь такую информацию во все кадры MP4 и получить новый MP4 в системах видеонаблюдения, инспекции, журналирования и аналитических интерфейсах встречается очень часто.
Но стоит начать работать с Media Foundation — и перед вами выстраиваются IMFSourceReader, IMFSample, IMFMediaBuffer, IMFTransform, IMFSinkWriter, и внезапно становится непонятно, в каком именно месте накладывать текст или PNG.
В этой статье сначала разберём общую картину — Source Reader -> отрисовка -> преобразование цвета -> Sink Writer, — а затем дадим однофайловый пример, который можно целиком вставить в консольное C++-приложение Visual Studio.
Пример читает заданный MP4, на каждом кадре рисует заданное изображение и строку HelloWorld и собирает выходной MP4.
При этом пример в первую очередь рассчитан на то, чтобы его можно было вставить и сразу запустить, поэтому перекодируется только видео. Remux звука тоже можно уместить в одну программу, но тема статьи — «прожечь изображение и текст в каждый кадр», поэтому сначала держимся только её.
flowchart TB
accTitle: Как сужен этот пример
accDescr: Пример в статье в первую очередь рассчитан на то, чтобы его можно было вставить и сразу запустить, поэтому перекодируется только видео, а remux звука откладывается на расширение после того, как основная тема — прожиг — уже проходит насквозь.
fo1["Сначала — вставить и запустить"] --> fo2["Перекодировать только видео"]
fo2 --> fo3["Сфокусироваться на прожиге кадров"]
fo1 -.-> fo4["remux звука оставить на потом"]
Рис. 1: Первая рабочая версия — минимальная схема, в которой проходит только тема прожига.
Код из этой статьи опубликован на GitHub полным набором примера (однофайловый .cpp и конфигурация сборки CMake).
media-foundation-overlay-image-text-on-mp4-frames - komurasoft-blog-samples (GitHub)
Для кого статья и что нужно для запуска
Текст рассчитан на разработчика среднего уровня, который начинает писать обработку видео на Windows на C++. Предполагается, что с основами COM (ComPtr, HRESULT, подсчёт ссылок) вы уже сталкивались, а Media Foundation для вас ещё новая тема.
Среда, без которой пример не соберётся, такая. Подробности и ограничения на входные данные собраны в главе 5.
| Параметр | Требование |
|---|---|
| ОС | Windows 10 / 11 |
| Среда разработки | консольное C++-приложение Visual Studio 2022 |
| Конфигурация сборки | x64 |
| Предкомпилированные заголовки | для этого .cpp поставить «Не использовать» |
| Входное видео | обычный MP4. Ширина и высота чётные (потому что NV12 — это 4:2:0) |
| Выход | MP4 только с видео. Звука не будет |
Термины, которые стоит знать заранее
С главы 3 в таблице появляются английские слова без пояснения. Зафиксируем их по одной строке.
| Термин | Смысл |
|---|---|
| remux | Пересобрать контейнер, не трогая сжатые данные внутри. Без перекодирования качество картинки и звука не падает, и обработка остаётся лёгкой |
| topology | Граф Media Foundation, который описывает, от какого компонента к какому текут данные. По сути схема, где соединены источник, преобразования и sink |
| custom MFT | Media Foundation Transform, который пишете сами. Реализовав IMFTransform, эффект можно вставить в конвейер Media Foundation как отдельный компонент |
| stride | Сколько байт занимает одна строка изображения в памяти. Это не всегда ширина × 4: в конце строки часто есть выравнивающий хвост |
1. Коротко — вывод
- Базовая схема, чтобы положить изображение или текст в каждый кадр MP4:
декодировать через Source Reader -> наложить на несжатые кадры -> при необходимости преобразовать цвет -> перекодировать через Sink Writer. - Само размещение изображения или текста — не задача Media Foundation. Здесь естественнее думать в терминах API отрисовки:
GDI+,Direct2D,DirectWrite,WIC. - Если результат нужно вернуть в
MP4 (H.264), почти всегда нужен этап преобразования между удобными для рисованияRGB32 / ARGB32и удобными для кодировщикаNV12 / I420 / YUY2. - Чтобы быстро получить первую рабочую версию, понятнее всего цепочка
Source Reader -> RGB32 -> отрисовка через GDI+ -> NV12 -> Sink Writer. - Если важнее скорость и запас по расширению, смещайтесь к
D3D11 / DXGI surface -> Direct2D / DirectWrite -> Video Processor MFT -> Sink Writer.
Карта знаний этой статьи
Статья описывает схему, в которой Media Foundation прожигает изображение и текст в каждый кадр MP4 и собирает новый MP4. Кадры декодируются в RGB32 через IMFSourceReader; в однофайловом примере GDI+ накладывает изображение и текст; поскольку H.264-кодировщик чаще всего ждёт вход семейства YUV вроде NV12, кадр преобразуют из BGRA в NV12, после чего IMFSinkWriter записывает его в MP4 как H.264. Различия stride и ориентации по вертикали снимаются через IMF2DBuffer::Lock2D и нормализуются в top-down BGRA; у ReadSample нужно проверять три вещи: HRESULT, flags и sample. Для production статья показывает поэтапное расширение: перенести преобразование цвета на Video Processor MFT, заменить отрисовку на Direct2D/DirectWrite, при необходимости переиспользования вынести логику в custom MFT; remux звука добавляют только после того, как прожиг видео уже стабильно работает.
flowchart LR
accTitle: Карта знаний: прожиг изображения и текста в MP4 (Media Foundation)
accDescr: Схема связей: декодирование RGB32 через IMFSourceReader и композитинг в GDI+, необходимость преобразования из семейства RGB в NV12 как входа H.264-кодировщика, запись MP4 через IMFSinkWriter, нормализация stride и ориентации top-down/bottom-up, расширение на Direct2D/DirectWrite, Video Processor MFT и custom MFT, а remux звука — как поздний шаг.
video_overlay_compositing["наложение изображения и текста на кадры видео"]
imfsinkwriter["IMFSinkWriter (Sink Writer)"]
imfsourcereader["IMFSourceReader (Source Reader)"]
gdiplus["GDI+"]
direct2d_directwrite["Direct2D / DirectWrite"]
video_stride["stride (байты на строку изображения)"]
imf2dbuffer_lock2d["IMF2DBuffer::Lock2D"]
top_down_bottom_up_orientation["ориентация top-down / bottom-up"]
mfvideoformat_rgb32["MFVideoFormat_RGB32"]
h264_video_encoder["H.264 Video Encoder (Media Foundation)"]
nv12_pixel_format["формат пикселей NV12"]
rgb_to_nv12_conversion["преобразование цвета BGRA → NV12"]
video_processor_mft["Video Processor MFT"]
readsample["IMFSourceReader::ReadSample"]
null_sample_result["null-сэмпл ReadSample"]
audio_remux["аудио remux"]
custom_mft["custom MFT"]
media_foundation["Media Foundation"]
video_overlay_compositing -->|"использует"| imfsourcereader
video_overlay_compositing -->|"использует"| gdiplus
direct2d_directwrite -.->|"рекомендуется для"| video_overlay_compositing
video_overlay_compositing -->|"требует"| video_stride
video_stride -->|"использует"| imf2dbuffer_lock2d
video_stride -->|"требует"| top_down_bottom_up_orientation
video_overlay_compositing -->|"использует"| mfvideoformat_rgb32
mfvideoformat_rgb32 -.->|"несовместимо с"| h264_video_encoder
h264_video_encoder -.->|"требует"| nv12_pixel_format
rgb_to_nv12_conversion -->|"требует"| mfvideoformat_rgb32
video_overlay_compositing -->|"использует"| rgb_to_nv12_conversion
video_processor_mft -->|"рекомендуется для"| rgb_to_nv12_conversion
imfsinkwriter -.->|"требует"| nv12_pixel_format
imfsinkwriter -.->|"использует"| h264_video_encoder
video_overlay_compositing -->|"требует"| imfsinkwriter
imfsourcereader -->|"использует"| readsample
readsample -->|"может вызвать"| null_sample_result
video_overlay_compositing -->|"должен предшествовать"| audio_remux
custom_mft -->|"рекомендуется для"| video_overlay_compositing
imfsourcereader -->|"требует"| media_foundation
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 20, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Почему задача кажется чуть запутаннее, чем есть
«Вписать текст в видео» на деле смешивает четыре разные темы.
-
Контейнер и кодек
mp4— это контейнер, а не сами кадры. Внутри обычно лежат сжатые данныеH.264илиH.265. -
Декодирование / кодирование Пока данные сжаты, обычный 2D API отрисовки не наложит на них ни текст, ни PNG. Сначала нужно вернуться к несжатым кадрам.
-
Отрисовка Текст, логотип, прозрачное наложение PNG, сглаженный текст — это не зона ответственности самой Media Foundation. Здесь работают
GDI+или связкаDirect2D / DirectWrite / WIC. -
Цветовое пространство и формат пикселей Формат, в котором удобно рисовать, и формат, который предпочитает кодировщик, не совпадают. Именно здесь тихо застревают.
Если сказать одной фразой, удобнее думать не «вписать текст средствами Media Foundation», а так: «Media Foundation прогоняет кадры, API отрисовки накладывает содержимое, перед кодированием при необходимости преобразуют цвет».
flowchart TB
accTitle: Четыре темы, которые смешаны вместе
accDescr: Требование вписать текст в видео смешивает четыре разные темы: контейнер и кодек, декодирование и кодирование, отрисовку, цветовое пространство и формат пикселей.
mixq["Вписать текст в видео"] --> t1["Контейнер и кодек"]
mixq --> t2["Декодирование и кодирование"]
mixq --> t3["Отрисовка"]
t3 -.-> t4["Цветовое пространство и формат пикселей"]
Рис. 2: Если застряли, сначала отделите, в какой из четырёх тем сейчас находитесь.
3. Сводная таблица, с которой стоит начать
| Подход | Схема | Когда уместно | На что смотреть |
|---|---|---|---|
| Сначала добиться корректной работы | Source Reader -> RGB32 -> композитинг -> NV12 -> Sink Writer |
Пакетная обработка, внутренние инструменты, первая реализация | На стороне CPU легко накапливаются копирования и преобразования |
| Поднять скорость | D3D11 / DXGI surface -> Direct2D / DirectWrite -> Video Processor MFT -> Sink Writer |
Длинные ролики, высокое разрешение, массовая обработка | Растёт объём управления D3D11 и DXGI |
| Сделать переиспользуемым компонентом | Реализовать как custom MFT и вставить в topology |
Эффект для нескольких приложений или встройка в конвейер MF | Сложнее реализация, регистрация и отладка |
Пример в статье ограничен верхней строкой — схемой «сначала добиться корректной работы».
3.1 Общая картина обработки
flowchart LR
A[input.mp4] --> B[IMFSourceReader]
B --> C[Несжатый кадр<br/>RGB32]
C --> D[GDI+ рисует изображение + HelloWorld]
D --> E[Преобразование BGRA -> NV12]
E --> F[IMFSinkWriter]
F --> G[output.mp4]
B --> H[Звуковые сэмплы]
H --> I[Копировать как есть<br/>или перекодировать]
I --> F
Рис. 3: Source Reader достаёт кадр, GDI+ рисует, преобразование уходит в NV12, Sink Writer пишет обратно.
Здесь важно, что сама отрисовка — не задача Media Foundation. Media Foundation отвечает за подачу и вывод кадров; изображение и текст кладёт API отрисовки.
4. Как разложить конвейер
4.1 Вход принимать через IMFSourceReader
Если на входе путь к файлу, понятнее всего MFCreateSourceReaderFromURL; если видео уже лежит в памяти — собрать IMFByteStream и вызвать MFCreateSourceReaderFromByteStream.
Первое решение здесь — принимать кадры в формате, удобном для рисования, или в формате, ориентированном на кодировщик.
- Если важнее простая реализация —
RGB32илиARGB32 - Если важнее эффективность кодирования — YUV вроде
NV12
Но композитинг текста и PNG гораздо проще мыслить в RGB, поэтому на старте проще принимать кадры как RGB32 / ARGB32.
flowchart TB
accTitle: С какого формата начинать приём
accDescr: Если важнее простота реализации, принимают RGB32 или ARGB32; если важнее эффективность кодирования — YUV вроде NV12. Композитинг текста и PNG проще мыслить в RGB, поэтому на старте удобнее принимать семейство RGB.
rq1{"Что важнее"}
rq1 -->|"Простота реализации"| rf1["Принимать RGB32 / ARGB32"]
rq1 -->|"Эффективность кодирования"| rf2["Принимать YUV вроде NV12"]
rf1 -.-> rf3["Композитинг проще мыслить в RGB"]
Рис. 4: Если сомневаетесь, начните с RGB — так проще рисовать.
Если включить MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, Source Reader сам сделает преобразование YUV -> RGB32 и деинтерлейсинг.
На этапе «хочу просто достать кадры и начать с ними работать» это удобно, но на длинных роликах и высоком разрешении режим легко становится тяжёлым. Если в production важна скорость, схему потом стоит пересмотреть.
flowchart TB
accTitle: Плюсы и минусы ENABLE_VIDEO_PROCESSING
accDescr: Если включить MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, Source Reader сам преобразует YUV в RGB32 и снимает чересстрочность, но на длинных роликах и высоком разрешении это легко становится тяжёлым, и в production ради скорости схему стоит пересмотреть.
ev1["Включить флаг"] --> ev2["Поручить преобразование YUV → RGB32"]
ev1 --> ev3["Поручить и деинтерлейсинг"]
ev2 -.-> ev4["На длинных роликах и высоком разрешении легко становится тяжело"]
Рис. 5: Флаг упрощает извлечение кадров, но за удобство платят скоростью.
4.2 Наложение изображения и текста — зона GDI+ или Direct2D / DirectWrite
Из IMFSample, который вернула Media Foundation, достают буфер и поверх него кладут логотип и текст.
Этот пример в первую очередь рассчитан на то, чтобы его было легко уместить в один файл, поэтому для отрисовки взят GDI+.
- Умеет загружать изображения
- Умеет рисовать текст
- Дополнительной подготовки сравнительно мало
- Легко укладывается в один
.cppконсольного приложения
С другой стороны, на длинных роликах и при массовой обработке 4K больше запаса у D3D11 + Direct2D + DirectWrite.
Естественный путь — начать с GDI+, а когда понадобится скорость, перейти на Direct2D / DirectWrite.
4.3 RGB32 не всегда можно сразу отдать в H.264
Это самое частое место, где всё застревает.
Когда результат возвращают в MP4 (H.264), H.264-кодировщик Microsoft чаще всего рассчитан на вход семейства YUV: I420 / IYUV / NV12 / YUY2 / YV12.
То есть скомпоновать кадр в удобных для рисования RGB32 / ARGB32 и просто отдать его в IMFSinkWriter — вариант, который не гарантирован.
На практике нужно одно из двух преобразований.
- Вставить
Video Processor MFT, который делаетRGB32 / ARGB32 -> NV12 - Написать своё преобразование
RGB -> NV12
Этот пример в первую очередь однофайловый, поэтому выбран второй путь — своё преобразование.
В production сильный вариант — вставить Video Processor MFT, который за один проход закрывает преобразование цветового пространства, смену размера и деинтерлейсинг.
flowchart TB
accTitle: Два пути из RGB в NV12
accDescr: После композитинга в удобных RGB32 или ARGB32 нужно либо преобразовать кадр через Video Processor MFT, либо написать своё преобразование RGB → NV12; пример ради однофайловости выбирает своё преобразование.
cv1["Композитинг в RGB закончен"] --> cv2["Преобразовать через Video Processor MFT"]
cv1 --> cv3["Преобразовать в NV12 самостоятельно"]
cv3 -.-> cv4["Пример выбирает однофайловость"]
cv2 -.-> cv5["В production это сильный вариант"]
Рис. 6: Этап преобразования нужен в любом случае; выбирают только, кому его отдать.
4.4 Выход писать через IMFSinkWriter
Для вывода видео удобнее всего IMFSinkWriter.
Идея простая:
- Тип выходного потока — формат, который нужно записать в файл
Пример:
MFVideoFormat_H264 - Тип входного потока — формат, который приложение отдаёт
Sink WriterПример:MFVideoFormat_NV12
Эти два параметра задают раздельно.
С точки зрения Sink Writer это выглядит так:
- приложение отдаёт несжатые кадры
NV12 Sink Writerкодирует их в H.264 и пишет в MP4
— такое разделение ролей.
flowchart TB
accTitle: Как Sink Writer разделяет типы входа и выхода
accDescr: У Sink Writer тип входного потока, который отдаёт приложение, задают как NV12, а тип выходного потока, который пишут в файл, — как H.264; кодирование берёт на себя Sink Writer и пишет MP4.
ap1["Приложение отдаёт кадры NV12"] --> sw1["Sink Writer"]
sw1 --> sw2["Кодирование в H.264"]
sw2 --> sw3["Запись в MP4"]
sw1 -.-> sw4["Тип входа и тип выхода задают раздельно"]
Рис. 7: У Sink Writer принято раздельно задавать формат, который отдаёте, и формат, который пишется в файл.
4.5 Звук на старте удобнее держать отдельно
Очень часто нужно только добавить логотип или текст на видео, а сам звук менять не хочется.
На практике удобна такая схема:
- видеопоток:
Source Reader -> композитинг -> Sink Writer - звуковой поток: remux в сжатом виде, без изменений
Но этот пример сфокусирован именно на прожиге изображения и текста в кадры, поэтому на выходе MP4 только с видео. Версию с сохранением звука проще добавить позже, на этапе расширения — так общую линию проще удержать в голове.
flowchart TB
accTitle: Видео и звук рассматривают раздельно
accDescr: На практике удобно гнать только видеопоток из Source Reader через композитинг в Sink Writer, а звуковой поток оставлять сжатым и делать remux; пример ради фокуса пишет только видео.
vs1["Видеопоток"] --> vs2["Композитинг и в Sink Writer"]
as1["Звуковой поток"] --> as2["remux в сжатом виде"]
as2 -.-> as3["В примере не обрабатывается"]
Рис. 8: Если менять нужно только картинку, звук не трогают и переносят вместе с контейнером.
5. Предпосылки примера и как им пользоваться
Предпосылки этого кода такие.
- Windows 10 / 11
- консольное C++-приложение Visual Studio 2022
- сборка
x64 - этот файл
.cppне использует предкомпилированные заголовки - ширина и высота входного видео чётные
- на входе обычный файл MP4
- на выходе MP4 только с видео
- изображение в формате, который умеет читать GDI+: PNG / JPEG / BMP / GIF и так далее
NV12 — это 4:2:0, поэтому ширина и высота должны быть чётными.
Если условие не выполнено, пример явно завершается ошибкой.
flowchart TB
accTitle: Почему ширина и высота должны быть чётными
accDescr: NV12 использует субдискретизацию 4:2:0, поэтому ширина и высота входного видео должны быть чётными; если условие не выполнено, пример явно останавливается с ошибкой.
nvq1["NV12 — это 4:2:0"] --> nvq2["Ширина и высота должны быть чётными"]
nvq2 --> nvq3{"Вход чётный?"}
nvq3 -->|"Чётные"| nvq4["Продолжить обработку"]
nvq3 -->|"Есть нечётный размер"| nvq5["Явно остановиться с ошибкой"]
Рис. 9: Лучше остановить неподходящий вход на пороге, чем молча получить испорченный результат.
5.1 Как пользоваться
- В Visual Studio создайте Console App
- Вставьте этот
.cppцеликом - Для этого
.cppотключите предкомпилированные заголовки — переключатель «Не использовать» - Соберите под
x64 - Запустите так
OverlayMp4.exe input.mp4 overlay.png output.mp4
input.mp4исходное видеоoverlay.pngизображение, которое нужно наложитьoutput.mp4куда сохранить результат
Строка задана в начале кода константой kOverlayText и равна HelloWorld.
Положение и размер тоже меняются константами в коде. Как вынести строку в аргумент командной строки — в разделе 9.5.
5.2 Как убедиться, что результат правильный
«Завершилось без ошибки» и «прожиг получился правильно» — разные вещи. Если пройти четыре проверки по порядку, большую часть сбоев видно сразу.
- Посмотрите число кадров при завершении. Пример по окончании печатает
Done. frames=и число записанных кадров. Если оно заметно расходится с общим числом кадров входного видео, кадры теряются где-то в циклеReadSample - Сравните базовые свойства выходного файла со входом. В Проводнике откройте выходной MP4: правый щелчок → Свойства → подробно. Там будут длительность, ширина кадра, высота кадра, частота кадров. Если длительность не совпадает со входом, смотрите обращение с timestamp (раздел 7.4)
- Глазами проверьте три места: начало, середину и конец. Если смотреть только первый кадр, легко пропустить исчезновение наложения где-то в середине. Надёжнее вырезать кадры в один и тот же момент из входа и выхода и положить рядом; процедура собрана в статье «Как извлечь кадр из MP4 по заданному времени с помощью Media Foundation»
- Проверьте, не «поехал» ли цвет. Если кожа или небо выглядят неестественно, возможно, выбор коэффициентов в
BgraToNv12(BT.601 и BT.709) не совпадает со входом
Для первого прогона лучше взять короткий MP4 на несколько секунд и PNG с чётким контуром. Если первую рабочую версию гонять на длинном ролике, на локализацию проблемы уходит слишком много времени.
flowchart TB
accTitle: Как проверить, что результат правильный
accDescr: Отсутствие ошибки и корректный прожиг — разные вещи, поэтому по порядку сверяют число кадров, сравнивают базовые свойства выходного файла, смотрят начало, середину и конец и проверяют цвет.
ck1["Сверить число кадров со входом"] --> ck2["Сравнить длительность и размер в свойствах"]
ck2 --> ck3["Просмотреть начало, середину и конец"]
ck3 --> ck4["Проверить неестественность цвета"]
ck4 -.-> ck5["Если цвет странный — смотреть выбор коэффициентов"]
Рис. 10: «Без ошибки» и «правильно» — не одно и то же, поэтому проверяют четыре стороны по порядку.
6. Однофайловый код, который вставляется в .cpp как есть
6.1 Карта кода
Сначала карта. Код длинный, но читать по делу нужно только три функции — CopySampleToTopDownBgra, DrawOverlay, BgraToNv12 — и цикл в wmain. Остальное — инициализация и разбор.
| Функция / класс | Роль | Подробнее |
|---|---|---|
ScopedMf / ScopedGdiplus |
MFStartup и инициализация/завершение GDI+ связаны через RAII |
— |
ConfigureSourceReader |
Выход Source Reader ставит в RGB32 и достаёт ширину, высоту, fps и длительность кадра по умолчанию |
4.1 |
GetDefaultStride |
Из медиатипа получает stride по умолчанию | 7.2 |
BufferLock |
Если есть IMF2DBuffer, блокирует его; иначе — IMFMediaBuffer |
7.2 |
CopySampleToTopDownBgra |
Снимает stride и ориентацию по вертикали и нормализует кадр в top-down BGRA | 7.2 |
DrawOverlay |
Рисует изображение и текст через GDI+. Это единственный «этап рисования» | 4.2 / 7.1 |
BgraToNv12 |
Преобразует уже нарисованный BGRA в NV12 | 4.3 / 7.1 |
CreateNv12Sample |
Оборачивает буфер NV12 в IMFSample и вешает timestamp и duration |
7.4 |
ChooseBitrate |
По данным входа выбирает битрейт выхода | — |
CreateSinkWriter |
Задаёт выходной тип H.264 и входной тип NV12, который отдаём мы |
4.4 |
цикл while в wmain |
Крутит по одному кадру, смотря HRESULT / flags / sample у ReadSample |
7.3 / 7.4 |
Если положить это рядом с картиной обработки из главы 3, соответствие такое: CopySampleToTopDownBgra — «несжатый кадр», DrawOverlay — «отрисовка через GDI+», BgraToNv12 — «преобразование BGRA → NV12».
flowchart TB
accTitle: Три функции, которые стоит читать
accDescr: В длинном коде по делу читают CopySampleToTopDownBgra, DrawOverlay, BgraToNv12 и цикл wmain: они отвечают за нормализацию в несжатый кадр, отрисовку и преобразование в NV12.
lp1["Цикл wmain гоняет по одному кадру"] --> fn1["CopySampleToTopDownBgra"]
fn1 --> fn2["DrawOverlay"]
fn2 --> fn3["BgraToNv12"]
fn1 -.-> ro1["Нормализация в несжатый кадр"]
fn2 -.-> ro2["Рисует изображение и текст"]
fn3 -.-> ro3["Готовит кадр к кодированию"]
Рис. 11: Остальное — инициализация и разбор; центр — эти три функции и цикл.
6.2 Код целиком
#define NOMINMAX
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <gdiplus.h>
#include <wrl/client.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cwchar>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "mf.lib")
#pragma comment(lib, "gdiplus.lib")
using Microsoft::WRL::ComPtr;
namespace
{
const wchar_t* kOverlayText = L"HelloWorld";
const float kMarginRatio = 0.03f;
const float kImageMaxWidthRatio = 0.20f;
const float kImageMaxHeightRatio = 0.20f;
const float kMinFontPx = 24.0f;
std::string HrToHex(HRESULT hr)
{
char buf[32]{};
std::snprintf(buf, sizeof(buf), "0x%08X", static_cast<unsigned int>(hr));
return std::string(buf);
}
void ThrowIfFailed(HRESULT hr, const char* message)
{
if (FAILED(hr))
{
throw std::runtime_error(std::string(message) + " failed. HRESULT=" + HrToHex(hr));
}
}
void ThrowIfGdiplusError(Gdiplus::Status status, const char* message)
{
if (status != Gdiplus::Ok)
{
char buf[128]{};
std::snprintf(buf, sizeof(buf), "%s failed. GDI+ status=%d", message, static_cast<int>(status));
throw std::runtime_error(buf);
}
}
BYTE ClampToByte(int value)
{
if (value < 0) return 0;
if (value > 255) return 255;
return static_cast<BYTE>(value);
}
class ScopedGdiplus
{
public:
ScopedGdiplus()
{
Gdiplus::GdiplusStartupInput input;
ThrowIfGdiplusError(Gdiplus::GdiplusStartup(&token_, &input, nullptr), "GdiplusStartup");
}
~ScopedGdiplus()
{
if (token_ != 0)
{
Gdiplus::GdiplusShutdown(token_);
}
}
private:
ULONG_PTR token_ = 0;
};
class ScopedMf
{
public:
ScopedMf()
{
ThrowIfFailed(CoInitializeEx(nullptr, COINIT_MULTITHREADED), "CoInitializeEx");
comInitialized_ = true;
ThrowIfFailed(MFStartup(MF_VERSION), "MFStartup");
mfStarted_ = true;
}
~ScopedMf()
{
if (mfStarted_)
{
MFShutdown();
}
if (comInitialized_)
{
CoUninitialize();
}
}
private:
bool comInitialized_ = false;
bool mfStarted_ = false;
};
class BufferLock
{
public:
explicit BufferLock(IMFMediaBuffer* buffer)
: buffer_(buffer)
{
if (!buffer_)
{
throw std::runtime_error("BufferLock received a null buffer.");
}
buffer_.As(&buffer2D_);
}
HRESULT LockBuffer(LONG defaultStride, DWORD heightInPixels, BYTE** scanline0, LONG* actualStride)
{
if (scanline0 == nullptr || actualStride == nullptr)
{
return E_POINTER;
}
HRESULT hr = S_OK;
if (buffer2D_)
{
hr = buffer2D_->Lock2D(scanline0, actualStride);
}
else
{
BYTE* data = nullptr;
hr = buffer_->Lock(&data, nullptr, nullptr);
if (SUCCEEDED(hr))
{
*actualStride = defaultStride;
if (defaultStride < 0)
{
*scanline0 = data + (static_cast<LONG>(heightInPixels) - 1) * std::abs(defaultStride);
}
else
{
*scanline0 = data;
}
}
}
locked_ = SUCCEEDED(hr);
return hr;
}
~BufferLock()
{
if (!locked_)
{
return;
}
if (buffer2D_)
{
buffer2D_->Unlock2D();
}
else
{
buffer_->Unlock();
}
}
private:
ComPtr<IMFMediaBuffer> buffer_;
ComPtr<IMF2DBuffer> buffer2D_;
bool locked_ = false;
};
struct VideoFormatInfo
{
UINT32 width = 0;
UINT32 height = 0;
UINT32 fpsNum = 0;
UINT32 fpsDen = 0;
UINT32 parNum = 1;
UINT32 parDen = 1;
LONG sourceStride = 0;
LONGLONG defaultFrameDuration = 0;
UINT32 bitrate = 0;
};
LONG GetDefaultStride(IMFMediaType* type)
{
LONG stride = 0;
HRESULT hr = type->GetUINT32(MF_MT_DEFAULT_STRIDE, reinterpret_cast<UINT32*>(&stride));
if (SUCCEEDED(hr))
{
return stride;
}
GUID subtype = GUID_NULL;
UINT32 width = 0;
UINT32 height = 0;
ThrowIfFailed(type->GetGUID(MF_MT_SUBTYPE, &subtype), "GetGUID(MF_MT_SUBTYPE)");
ThrowIfFailed(MFGetAttributeSize(type, MF_MT_FRAME_SIZE, &width, &height), "MFGetAttributeSize(MF_MT_FRAME_SIZE)");
ThrowIfFailed(MFGetStrideForBitmapInfoHeader(subtype.Data1, width, &stride), "MFGetStrideForBitmapInfoHeader");
ThrowIfFailed(type->SetUINT32(MF_MT_DEFAULT_STRIDE, static_cast<UINT32>(stride)), "SetUINT32(MF_MT_DEFAULT_STRIDE)");
return stride;
}
UINT32 ChooseBitrate(IMFMediaType* nativeType, UINT32 width, UINT32 height, UINT32 fpsNum, UINT32 fpsDen)
{
UINT32 srcBitrate = 0;
if (SUCCEEDED(nativeType->GetUINT32(MF_MT_AVG_BITRATE, &srcBitrate)) && srcBitrate > 0)
{
return srcBitrate;
}
const double fps = static_cast<double>(fpsNum) / static_cast<double>(fpsDen);
double estimated = static_cast<double>(width) * static_cast<double>(height) * fps * 0.07;
if (estimated < 1500000.0)
{
estimated = 1500000.0;
}
if (estimated > 25000000.0)
{
estimated = 25000000.0;
}
return static_cast<UINT32>(estimated);
}
VideoFormatInfo ConfigureSourceReader(IMFSourceReader* reader)
{
ThrowIfFailed(reader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE), "SetStreamSelection(all,false)");
ThrowIfFailed(reader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE), "SetStreamSelection(video,true)");
ComPtr<IMFMediaType> nativeType;
ThrowIfFailed(reader->GetNativeMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &nativeType), "GetNativeMediaType(video)");
ComPtr<IMFMediaType> requestedType;
ThrowIfFailed(MFCreateMediaType(&requestedType), "MFCreateMediaType(video requested)");
ThrowIfFailed(requestedType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video), "SetGUID(video requested major)");
ThrowIfFailed(requestedType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32), "SetGUID(video requested subtype RGB32)");
ThrowIfFailed(reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, requestedType.Get()), "SetCurrentMediaType(video RGB32)");
ComPtr<IMFMediaType> currentType;
ThrowIfFailed(reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, ¤tType), "GetCurrentMediaType(video)");
VideoFormatInfo info;
ThrowIfFailed(MFGetAttributeSize(currentType.Get(), MF_MT_FRAME_SIZE, &info.width, &info.height), "Get video frame size");
HRESULT hr = MFGetAttributeRatio(currentType.Get(), MF_MT_FRAME_RATE, &info.fpsNum, &info.fpsDen);
if (FAILED(hr))
{
ThrowIfFailed(MFGetAttributeRatio(nativeType.Get(), MF_MT_FRAME_RATE, &info.fpsNum, &info.fpsDen), "Get video frame rate");
}
if (info.fpsNum == 0 || info.fpsDen == 0)
{
throw std::runtime_error("Video frame rate is zero.");
}
hr = MFGetAttributeRatio(currentType.Get(), MF_MT_PIXEL_ASPECT_RATIO, &info.parNum, &info.parDen);
if (FAILED(hr) || info.parNum == 0 || info.parDen == 0)
{
info.parNum = 1;
info.parDen = 1;
}
info.sourceStride = GetDefaultStride(currentType.Get());
info.defaultFrameDuration = (10000000LL * info.fpsDen) / info.fpsNum;
if (info.defaultFrameDuration <= 0)
{
throw std::runtime_error("Calculated frame duration is invalid.");
}
info.bitrate = ChooseBitrate(nativeType.Get(), info.width, info.height, info.fpsNum, info.fpsDen);
return info;
}
ComPtr<IMFSinkWriter> CreateSinkWriter(const std::wstring& outputPath, const VideoFormatInfo& videoInfo, DWORD* streamIndex)
{
if (streamIndex == nullptr)
{
throw std::runtime_error("streamIndex is null.");
}
ComPtr<IMFAttributes> attributes;
ThrowIfFailed(MFCreateAttributes(&attributes, 1), "MFCreateAttributes(sink)");
ThrowIfFailed(attributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE), "SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS)");
ComPtr<IMFSinkWriter> writer;
ThrowIfFailed(MFCreateSinkWriterFromURL(outputPath.c_str(), nullptr, attributes.Get(), &writer), "MFCreateSinkWriterFromURL");
ComPtr<IMFMediaType> outputType;
ThrowIfFailed(MFCreateMediaType(&outputType), "MFCreateMediaType(video output)");
ThrowIfFailed(outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video), "SetGUID(output major)");
ThrowIfFailed(outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264), "SetGUID(output subtype H264)");
ThrowIfFailed(outputType->SetUINT32(MF_MT_AVG_BITRATE, videoInfo.bitrate), "SetUINT32(output bitrate)");
ThrowIfFailed(outputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive), "SetUINT32(output interlace)");
ThrowIfFailed(MFSetAttributeSize(outputType.Get(), MF_MT_FRAME_SIZE, videoInfo.width, videoInfo.height), "MFSetAttributeSize(output frame size)");
ThrowIfFailed(MFSetAttributeRatio(outputType.Get(), MF_MT_FRAME_RATE, videoInfo.fpsNum, videoInfo.fpsDen), "MFSetAttributeRatio(output fps)");
ThrowIfFailed(MFSetAttributeRatio(outputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, videoInfo.parNum, videoInfo.parDen), "MFSetAttributeRatio(output PAR)");
ThrowIfFailed(writer->AddStream(outputType.Get(), streamIndex), "AddStream(video)");
ComPtr<IMFMediaType> inputType;
ThrowIfFailed(MFCreateMediaType(&inputType), "MFCreateMediaType(video input)");
ThrowIfFailed(inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video), "SetGUID(input major)");
ThrowIfFailed(inputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12), "SetGUID(input subtype NV12)");
ThrowIfFailed(inputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive), "SetUINT32(input interlace)");
ThrowIfFailed(MFSetAttributeSize(inputType.Get(), MF_MT_FRAME_SIZE, videoInfo.width, videoInfo.height), "MFSetAttributeSize(input frame size)");
ThrowIfFailed(MFSetAttributeRatio(inputType.Get(), MF_MT_FRAME_RATE, videoInfo.fpsNum, videoInfo.fpsDen), "MFSetAttributeRatio(input fps)");
ThrowIfFailed(MFSetAttributeRatio(inputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, videoInfo.parNum, videoInfo.parDen), "MFSetAttributeRatio(input PAR)");
ThrowIfFailed(writer->SetInputMediaType(*streamIndex, inputType.Get(), nullptr), "SetInputMediaType(video)");
ThrowIfFailed(writer->BeginWriting(), "BeginWriting");
return writer;
}
void CopySampleToTopDownBgra(IMFSample* sample, const VideoFormatInfo& videoInfo, std::vector<BYTE>& bgra)
{
ComPtr<IMFMediaBuffer> buffer;
ThrowIfFailed(sample->ConvertToContiguousBuffer(&buffer), "ConvertToContiguousBuffer");
BufferLock lock(buffer.Get());
BYTE* scanline0 = nullptr;
LONG actualStride = 0;
ThrowIfFailed(lock.LockBuffer(videoInfo.sourceStride, videoInfo.height, &scanline0, &actualStride), "LockBuffer");
const size_t dstStride = static_cast<size_t>(videoInfo.width) * 4;
bgra.resize(dstStride * videoInfo.height);
for (UINT32 y = 0; y < videoInfo.height; ++y)
{
const BYTE* srcRow = scanline0 + static_cast<LONG>(y) * actualStride;
BYTE* dstRow = bgra.data() + static_cast<size_t>(y) * dstStride;
std::memcpy(dstRow, srcRow, dstStride);
for (UINT32 x = 0; x < videoInfo.width; ++x)
{
dstRow[static_cast<size_t>(x) * 4 + 3] = 0xFF;
}
}
}
void DrawOverlay(std::vector<BYTE>& bgra, UINT32 width, UINT32 height, Gdiplus::Image& overlayImage)
{
const INT stride = static_cast<INT>(width * 4);
Gdiplus::Bitmap frameBitmap(
static_cast<INT>(width),
static_cast<INT>(height),
stride,
PixelFormat32bppPARGB,
bgra.data());
ThrowIfGdiplusError(frameBitmap.GetLastStatus(), "Create frame bitmap");
Gdiplus::Graphics graphics(&frameBitmap);
ThrowIfGdiplusError(graphics.GetLastStatus(), "Create graphics");
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
graphics.SetCompositingQuality(Gdiplus::CompositingQualityHighQuality);
graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAliasGridFit);
const Gdiplus::REAL margin = std::max<Gdiplus::REAL>(16.0f, static_cast<Gdiplus::REAL>(height) * kMarginRatio);
const Gdiplus::REAL maxImageW = static_cast<Gdiplus::REAL>(width) * kImageMaxWidthRatio;
const Gdiplus::REAL maxImageH = static_cast<Gdiplus::REAL>(height) * kImageMaxHeightRatio;
const Gdiplus::REAL srcW = static_cast<Gdiplus::REAL>(overlayImage.GetWidth());
const Gdiplus::REAL srcH = static_cast<Gdiplus::REAL>(overlayImage.GetHeight());
if (srcW <= 0.0f || srcH <= 0.0f)
{
throw std::runtime_error("Overlay image has invalid size.");
}
const Gdiplus::REAL imageScale =
std::min<Gdiplus::REAL>(1.0f, std::min(maxImageW / srcW, maxImageH / srcH));
const Gdiplus::REAL drawW = srcW * imageScale;
const Gdiplus::REAL drawH = srcH * imageScale;
Gdiplus::RectF imageRect(margin, margin, drawW, drawH);
Gdiplus::SolidBrush imagePlate(Gdiplus::Color(96, 0, 0, 0));
graphics.FillRectangle(
&imagePlate,
imageRect.X - 8.0f,
imageRect.Y - 8.0f,
imageRect.Width + 16.0f,
imageRect.Height + 16.0f);
graphics.DrawImage(&overlayImage, imageRect);
const Gdiplus::REAL fontPx =
std::max<Gdiplus::REAL>(kMinFontPx, static_cast<Gdiplus::REAL>(height) * 0.06f);
Gdiplus::Font font(L"Segoe UI", fontPx, Gdiplus::FontStyleBold, Gdiplus::UnitPixel);
ThrowIfGdiplusError(font.GetLastStatus(), "Create font");
Gdiplus::StringFormat stringFormat;
stringFormat.SetAlignment(Gdiplus::StringAlignmentNear);
stringFormat.SetLineAlignment(Gdiplus::StringAlignmentNear);
Gdiplus::RectF measureLayout(
margin,
static_cast<Gdiplus::REAL>(height) - margin - fontPx * 2.0f,
static_cast<Gdiplus::REAL>(width) - margin * 2.0f,
fontPx * 2.0f);
Gdiplus::RectF measured;
graphics.MeasureString(kOverlayText, -1, &font, measureLayout, &stringFormat, &measured);
Gdiplus::RectF textBg(
measured.X - 12.0f,
measured.Y - 8.0f,
measured.Width + 24.0f,
measured.Height + 16.0f);
Gdiplus::SolidBrush textPlate(Gdiplus::Color(128, 0, 0, 0));
graphics.FillRectangle(&textPlate, textBg);
Gdiplus::SolidBrush shadowBrush(Gdiplus::Color(220, 0, 0, 0));
Gdiplus::RectF shadowLayout = measureLayout;
shadowLayout.X += 2.0f;
shadowLayout.Y += 2.0f;
graphics.DrawString(kOverlayText, -1, &font, shadowLayout, &stringFormat, &shadowBrush);
Gdiplus::SolidBrush textBrush(Gdiplus::Color(235, 255, 255, 255));
graphics.DrawString(kOverlayText, -1, &font, measureLayout, &stringFormat, &textBrush);
}
void BgraToNv12(const BYTE* bgra, UINT32 width, UINT32 height, BYTE* nv12)
{
const bool useBt709 = (width > 1024 || height > 576);
const int yR = useBt709 ? 47 : 66;
const int yG = useBt709 ? 157 : 129;
const int yB = useBt709 ? 16 : 25;
const int uR = useBt709 ? -26 : -38;
const int uG = useBt709 ? -87 : -74;
const int uB = 112;
const int vR = 112;
const int vG = useBt709 ? -102 : -94;
const int vB = useBt709 ? -10 : -18;
BYTE* yPlane = nv12;
BYTE* uvPlane = nv12 + static_cast<size_t>(width) * height;
const size_t srcStride = static_cast<size_t>(width) * 4;
for (UINT32 y = 0; y < height; ++y)
{
const BYTE* srcRow = bgra + static_cast<size_t>(y) * srcStride;
BYTE* dstY = yPlane + static_cast<size_t>(y) * width;
for (UINT32 x = 0; x < width; ++x)
{
const BYTE b = srcRow[x * 4 + 0];
const BYTE g = srcRow[x * 4 + 1];
const BYTE r = srcRow[x * 4 + 2];
const int Y = ((yR * r + yG * g + yB * b + 128) >> 8) + 16;
dstY[x] = ClampToByte(Y);
}
}
for (UINT32 y = 0; y < height; y += 2)
{
const BYTE* row0 = bgra + static_cast<size_t>(y) * srcStride;
const BYTE* row1 = bgra + static_cast<size_t>(y + 1) * srcStride;
BYTE* dstUV = uvPlane + static_cast<size_t>(y / 2) * width;
for (UINT32 x = 0; x < width; x += 2)
{
int b = 0;
int g = 0;
int r = 0;
for (UINT32 dy = 0; dy < 2; ++dy)
{
const BYTE* row = (dy == 0) ? row0 : row1;
for (UINT32 dx = 0; dx < 2; ++dx)
{
const UINT32 ix = x + dx;
b += row[ix * 4 + 0];
g += row[ix * 4 + 1];
r += row[ix * 4 + 2];
}
}
b = (b + 2) / 4;
g = (g + 2) / 4;
r = (r + 2) / 4;
const int U = ((uR * r + uG * g + uB * b + 128) >> 8) + 128;
const int V = ((vR * r + vG * g + vB * b + 128) >> 8) + 128;
dstUV[x + 0] = ClampToByte(U);
dstUV[x + 1] = ClampToByte(V);
}
}
}
ComPtr<IMFSample> CreateNv12Sample(
const std::vector<BYTE>& bgra,
const VideoFormatInfo& videoInfo,
LONGLONG sampleTime,
LONGLONG sampleDuration)
{
const DWORD bufferSize =
static_cast<DWORD>(videoInfo.width * videoInfo.height * 3 / 2);
ComPtr<IMFMediaBuffer> buffer;
ThrowIfFailed(MFCreateMemoryBuffer(bufferSize, &buffer), "MFCreateMemoryBuffer");
BYTE* dst = nullptr;
DWORD maxLength = 0;
DWORD currentLength = 0;
ThrowIfFailed(buffer->Lock(&dst, &maxLength, ¤tLength), "Lock(NV12 buffer)");
try
{
BgraToNv12(bgra.data(), videoInfo.width, videoInfo.height, dst);
}
catch (...)
{
buffer->Unlock();
throw;
}
ThrowIfFailed(buffer->Unlock(), "Unlock(NV12 buffer)");
ThrowIfFailed(buffer->SetCurrentLength(bufferSize), "SetCurrentLength(NV12 buffer)");
ComPtr<IMFSample> sample;
ThrowIfFailed(MFCreateSample(&sample), "MFCreateSample");
ThrowIfFailed(sample->AddBuffer(buffer.Get()), "AddBuffer(output sample)");
ThrowIfFailed(sample->SetSampleTime(sampleTime), "SetSampleTime");
ThrowIfFailed(sample->SetSampleDuration(sampleDuration), "SetSampleDuration");
return sample;
}
}
int wmain(int argc, wchar_t* argv[])
{
if (argc != 4)
{
std::wcerr << L"Usage: OverlayMp4.exe <input.mp4> <overlayImage.png> <output.mp4>" << std::endl;
return 1;
}
const std::wstring inputPath = argv[1];
const std::wstring imagePath = argv[2];
const std::wstring outputPath = argv[3];
try
{
if (_wcsicmp(inputPath.c_str(), outputPath.c_str()) == 0)
{
throw std::runtime_error("Input and output paths must be different.");
}
ScopedMf mf;
ScopedGdiplus gdiplus;
ComPtr<IMFAttributes> readerAttributes;
ThrowIfFailed(MFCreateAttributes(&readerAttributes, 1), "MFCreateAttributes(reader)");
ThrowIfFailed(
readerAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE),
"SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING)");
ComPtr<IMFSourceReader> reader;
ThrowIfFailed(
MFCreateSourceReaderFromURL(inputPath.c_str(), readerAttributes.Get(), &reader),
"MFCreateSourceReaderFromURL");
VideoFormatInfo videoInfo = ConfigureSourceReader(reader.Get());
if ((videoInfo.width % 2) != 0 || (videoInfo.height % 2) != 0)
{
throw std::runtime_error(
"This sample requires even video width and height because NV12 is 4:2:0.");
}
Gdiplus::Image overlayImage(imagePath.c_str());
ThrowIfGdiplusError(overlayImage.GetLastStatus(), "Load overlay image");
DWORD videoStreamIndex = 0;
ComPtr<IMFSinkWriter> writer =
CreateSinkWriter(outputPath, videoInfo, &videoStreamIndex);
std::vector<BYTE> bgra;
LONGLONG firstTimestamp = -1;
unsigned long long frameCount = 0;
while (true)
{
DWORD flags = 0;
LONGLONG timestamp = 0;
ComPtr<IMFSample> inputSample;
ThrowIfFailed(
reader->ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
0,
nullptr,
&flags,
×tamp,
&inputSample),
"ReadSample(video)");
if ((flags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) != 0)
{
throw std::runtime_error("Dynamic video format change is not supported in this sample.");
}
if ((flags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED) != 0)
{
throw std::runtime_error("Native video format change is not supported in this sample.");
}
if ((flags & MF_SOURCE_READERF_STREAMTICK) != 0)
{
if (firstTimestamp < 0)
{
firstTimestamp = timestamp;
}
ThrowIfFailed(
writer->SendStreamTick(videoStreamIndex, timestamp - firstTimestamp),
"SendStreamTick");
}
if (inputSample)
{
if (firstTimestamp < 0)
{
firstTimestamp = timestamp;
}
LONGLONG duration = 0;
if (FAILED(inputSample->GetSampleDuration(&duration)) || duration <= 0)
{
duration = videoInfo.defaultFrameDuration;
}
CopySampleToTopDownBgra(inputSample.Get(), videoInfo, bgra);
DrawOverlay(bgra, videoInfo.width, videoInfo.height, overlayImage);
ComPtr<IMFSample> outputSample =
CreateNv12Sample(bgra, videoInfo, timestamp - firstTimestamp, duration);
ThrowIfFailed(
writer->WriteSample(videoStreamIndex, outputSample.Get()),
"WriteSample(video)");
++frameCount;
}
if ((flags & MF_SOURCE_READERF_ENDOFSTREAM) != 0)
{
break;
}
}
ThrowIfFailed(writer->Finalize(), "Finalize");
std::wcout
<< L"Done. frames=" << frameCount
<< L", output=" << outputPath
<< std::endl;
return 0;
}
catch (const std::exception& ex)
{
std::cerr << ex.what() << std::endl;
return 1;
}
}
7. На что смотреть, читая эту реализацию
7.1 Формат, удобный для рисования, и формат, удобный для кодировщика, — разные вещи
В этом примере поток такой:
- выход
Source Reader:RGB32 - отрисовка:
GDI+ - вход
Sink Writer:NV12
Причина простая: текст и PNG удобнее класть в RGB, а в H.264-кодировщик удобнее отдавать NV12.
Читая реализацию, проще следить за кодом, если мысленно разделить его на «этап рисования» и «этап подготовки к кодированию».
7.2 Stride и ориентацию по вертикали нормализуют до отрисовки
Кадры видео в памяти не обязаны лежать так, как выглядят на экране.
- stride может не совпадать с
width * 4 - кадр может быть перевёрнут сверху вниз
IMF2DBufferиIMFMediaBufferобрабатываются чуть по-разному
Поэтому этот код сначала нормализует кадр в буфер BGRA с порядком строк top-down и только потом рисует. Если свести этот разнобой к одному виду заранее, код отрисовки получается заметно прямее.
flowchart TB
accTitle: Зачем нормализовать до рисования
accDescr: Расхождения вроде stride, который не равен ширине × 4, перевёрнутой ориентации и разного обращения с IMF2DBuffer и IMFMediaBuffer сначала снимают нормализацией в top-down BGRA и только потом рисуют.
ir1["stride может не совпасть"] --> nr1["Нормализация в top-down BGRA"]
ir2["Кадр бывает перевёрнут"] --> nr1
ir3["Тип буфера меняет обращение"] --> nr1
nr1 --> nr2["Код отрисовки становится прямым"]
Рис. 12: Если разнобой в памяти снять в одном месте, стороне рисования ничего знать не нужно.
7.3 У ReadSample смотрят не только HRESULT, но и flags вместе с sample
ReadSample может вернуть S_OK, а sample == nullptr.
Типичные случаи:
MF_SOURCE_READERF_STREAMTICKMF_SOURCE_READERF_ENDOFSTREAM- прочие потоковые события
Поэтому в цикле нужно смотреть сразу три значения: HRESULT, flags и inputSample.
Если пропустить STREAMTICK или ENDOFSTREAM, дальнейшая обработка временной шкалы легко разъезжается.
flowchart TB
accTitle: Три точки, которые смотрят у ReadSample
accDescr: ReadSample может вернуть S_OK при sample равном nullptr; типичны потоковые события вроде STREAMTICK и ENDOFSTREAM, поэтому в цикле проверяют HRESULT, flags и sample вместе.
rs1["ReadSample вернулся"] --> rs2["Проверить HRESULT"]
rs2 --> rs3["Проверить flags"]
rs3 --> rs4["Проверить, есть ли sample"]
rs4 -.-> rs5["S_OK не гарантирует ненулевой указатель"]
Рис. 13: Один код возврата ничего не решает — кадр обрабатывают как набор из трёх проверок.
7.4 Timestamp и duration безопаснее наследовать со входа
Метка времени задаётся в единицах по 100 нс.
Duration при этом нужно отдельно брать из IMFSample.
Надёжнее по возможности наследовать timestamp / duration входного sample, чем каждый раз прибавлять константу из расчёта на фиксированный fps.
В этом примере значение по умолчанию, посчитанное из fps, используется только когда duration получить не удалось.
flowchart TB
accTitle: Как обращаются с timestamp и duration
accDescr: Вместо того чтобы прибавлять константу из расчёта на фиксированный fps, timestamp и duration по возможности наследуют из входного sample, а к значению по умолчанию из fps падают только если duration получить не удалось.
ts1["Взять из входного sample"] --> ts2{"duration удалось получить?"}
ts2 -->|"Да"| ts3["Наследовать как есть"]
ts2 -->|"Нет"| ts4["Взять значение по умолчанию из fps"]
ts3 -.-> ts5["Надёжнее, чем прибавлять константу"]
Рис. 14: Время не изобретают — его наследуют со входа.
7.5 GDI+ легко внедрить, но на длинных роликах и высоком разрешении есть следующий этап
GDI+ отлично подходит для однофайлового примера, но на длинных роликах и при массовой обработке 4K связка D3D11 + Direct2D + DirectWrite часто оказывается выгоднее.
- сначала прогнать весь конвейер на
GDI+ - затем при необходимости заменить на
Direct2D / DirectWrite - преобразование цвета увести в
Video Processor MFTили на GPU
Такое поэтапное движение позволяет расширять решение, не ломая схему.
flowchart TB
accTitle: Как поэтапно менять API отрисовки
accDescr: Сначала весь конвейер прогоняют на GDI+, при необходимости заменяют на Direct2D и DirectWrite, а преобразование цвета уводят в Video Processor MFT или на GPU — так схему не ломают.
gd1["Сначала прогнать всё на GDI+"] --> gd2["При необходимости заменить на Direct2D"]
gd2 --> gd3["Преобразование цвета — на MFT или GPU"]
gd1 -.-> gd4["Длинные ролики и массовый 4K — следующий этап"]
Рис. 15: Начинают с того, что проще внедрить, и по мере нужды в производительности заменяют по частям.
7.6 Этот пример сфокусирован только на видео
Если в ту же статью напихать ещё и полноценную работу со звуком, основная мысль легко расплывается. Поэтому пример держит фокус на прожиге изображения и текста в кадры видео, а на выходе получается MP4 только с видео.
На практике его логично вырастить до схемы:
- видео:
Source Reader -> композитинг -> Sink Writer - звук: remux в сжатом виде, без изменений
8. Если «переданные видеоданные» — не файл, а байты MP4 в памяти
Этот код вызывает MFCreateSourceReaderFromURL, поэтому на входе путь к файлу.
Но если требование звучит как «нужно сделать то же самое с байтами mp4, которые пришли по API», сама логика не меняется. Меняется только точка входа.
- подготовить
IStreamили собственный поток - отдать его в
Source ReaderкакIMFByteStream - дальше всё то же:
RGB32 -> отрисовка -> NV12 -> Sink Writer
Иначе говоря, суть не в том, как хранятся видеоданные, а в том, как рисовать поверх каждого декодированного кадра.
flowchart TB
accTitle: Если вход — байтовый массив, меняется только вход
accDescr: Если вход — путь к файлу, вызывают MFCreateSourceReaderFromURL; если это байты MP4 в памяти, готовят IMFByteStream и отдают его Source Reader; дальше цепочка RGB32 → отрисовка → NV12 → Sink Writer та же.
in1["Путь к файлу"] --> sr1["Source Reader"]
in2["Байтовый массив в памяти"] --> bs1["Обернуть в IMFByteStream"]
bs1 --> sr1
sr1 --> same1["Дальше поток тот же"]
Рис. 16: Способ хранения данных может измениться — меняется только один входной шаг.
9. Как расти до production
9.1 Добавить remux звука
Самое практичное первое расширение — оставить звук как есть. Если перекодировать только видео, а звук записать обратно в том же сжатом формате, требование закрывается без заметного роста объёма реализации.
Sink Writer явно поддерживает связку сжатый вход того же формата, что и выход, именно как remux без перекодирования. Добавляют три места.
- Принять звуковой поток сжатым. Включают его через
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE)и тип, полученный изGetNativeMediaType, как есть передают вSetCurrentMediaType. Если декодировать не хотите, на стороне Source Reader принято указывать native-тип - На Sink Writer тот же тип ставят и на вход, и на выход. В
writer->AddStream(audioType.Get(), &audioStreamIndex)иwriter->SetInputMediaType(audioStreamIndex, audioType.Get(), nullptr)передают один и тот же медиатип - Ось timestamp сделать общей с видео.
firstTimestamp, который код в 6.2 уже использует для видео, так же вычитают и у звуковых сэмплов. Если завести две разные оси, звук разъедется с картинкой
Вызов ReadSample тоже меняют: сейчас читается только видео, а нужно перейти на MF_SOURCE_READER_ANY_STREAM и разводить потоки по индексу, который возвращает вызов.
Sink Writer сам не делает ресемплинг звука и не меняет размер кадра или частоту кадров видео, если этого не даёт кодировщик. Если sink MP4 не принимает входной формат звука, нужен не remux, а перекодирование.
flowchart TB
accTitle: Три места, которые добавляют для remux звука
accDescr: Чтобы оставить звук как есть, звуковой поток принимают сжатым, на Sink Writer тот же тип ставят и на вход, и на выход, и ось timestamp делают общей с видео.
ar1["Принять звук сжатым"] --> ar2["Тот же тип на вход и на выход"]
ar2 --> ar3["Общая ось timestamp с видео"]
ar3 -.-> ar4["Разные оси — звук разъедется"]
Рис. 17: Remux добавляется в трёх местах, но общую ось времени забывать нельзя.
9.2 Вставить Video Processor MFT
Этот пример ради однофайловости сам преобразует BGRA -> NV12, но в production схема с Video Processor MFT тоже очень сильный вариант.
С Video Processor MFT проще закрыть за один раз:
- преобразование цветового пространства
- смену размера
- деинтерлейсинг
- преобразование частоты кадров
9.3 Заменить GDI+ на Direct2D / DirectWrite
Для наложений вроде логотипа, субтитров или метки времени GDI+ во многих случаях достаточно, но если ужимать производительность, выгоднее Direct2D / DirectWrite.
Особенно если есть такие условия, как
- высокое разрешение
- большая длительность
- большой объём роликов
- планы со временем уйти на GPU-путь
— тогда в поле зрения попадает схема на D3D11 / DXGI surface.
9.4 Custom MFT имеет смысл, когда нужен «видеоэффект, который будут переиспользовать»
В Media Foundation эффект можно реализовать как IMFTransform.
Поэтому если одно и то же наложение нужно в нескольких приложениях или pipeline, custom MFT — аккуратный выбор.
Но для первой рабочей версии стоит помнить, что:
- нужно удовлетворить контракт
IMFTransform - растёт объём управления входными и выходными медиатипами
- сложнее регистрация и отладка
Поэтому на практике чаще проще сначала добиться корректной работы на связке Source Reader + композитинг + Sink Writer и вынести MFT, только когда в этом реально появится нужда.
flowchart TB
accTitle: Когда рассматривать custom MFT
accDescr: Сначала добиваются корректной работы на Source Reader, композитинге и Sink Writer; в custom MFT выносят, когда то же наложение начинают переиспользовать в нескольких приложениях или конвейерах.
mf1["Сначала добиться корректной работы"] --> mf2{"Нужно переиспользовать?"}
mf2 -->|"В нескольких приложениях"| mf3["Вынести в custom MFT"]
mf2 -->|"Хватает одной программы"| mf4["Оставить текущую схему"]
mf3 -.-> mf5["Сложнее реализация, регистрация и отладка"]
Рис. 18: Вынос в компонент выглядит аккуратно, но не опоздаете, если сделаете его по факту нужды.
9.5 Вынести строку в аргумент и научиться рисовать японский текст
В примере строка зафиксирована в kOverlayText как HelloWorld. Если прожигать номер оборудования или имя оператора, первым делом хочется сделать её аргументом. Для японского текста нужно ещё сменить шрифт.
Меняют четыре места.
1. В безымянном namespace добавляют имя шрифта. Существующий kOverlayText оставляют значением по умолчанию.
const wchar_t* kOverlayText = L"HelloWorld"; // уже есть; значение по умолчанию, если аргумент не передан
const wchar_t* kFontFamilyName = L"Yu Gothic UI"; // гарнитура, в которой есть японские глифы
const wchar_t* kFallbackFontFamilyName = L"Segoe UI"; // если верхней нет в системе
2. В DrawOverlay добавляют строку, которую нужно нарисовать.
void DrawOverlay(
std::vector<BYTE>& bgra,
UINT32 width,
UINT32 height,
Gdiplus::Image& overlayImage,
const std::wstring& overlayText) // добавлено
3. Внутри DrawOverlay меняют создание шрифта и то, откуда берётся строка. Строку Gdiplus::Font font(L"Segoe UI", ...) заменяют на следующее.
// Если указанной гарнитуры нет, падаем на гарнитуру по умолчанию
Gdiplus::FontFamily preferred(kFontFamilyName);
Gdiplus::FontFamily fallback(kFallbackFontFamilyName);
const Gdiplus::FontFamily& family = preferred.IsAvailable() ? preferred : fallback;
if (!family.IsAvailable())
{
throw std::runtime_error("Neither the preferred nor the fallback font family is installed.");
}
Gdiplus::Font font(&family, fontPx, Gdiplus::FontStyleBold, Gdiplus::UnitPixel);
ThrowIfGdiplusError(font.GetLastStatus(), "Create font");
После этого все вхождения kOverlayText, которые уходят в MeasureString и в два вызова DrawString, заменяют на overlayText.c_str(). Если поправить не все три места, в тени останется старая строка.
4. В wmain принимают аргумент и передают его в DrawOverlay.
if (argc < 4 || argc > 5)
{
std::wcerr
<< L"Usage: OverlayMp4.exe <input.mp4> <overlayImage.png> <output.mp4> [text]"
<< std::endl;
return 1;
}
const std::wstring inputPath = argv[1];
const std::wstring imagePath = argv[2];
const std::wstring outputPath = argv[3];
const std::wstring overlayText = (argc == 5) ? std::wstring(argv[4]) : std::wstring(kOverlayText);
А вызов внутри цикла меняют так.
DrawOverlay(bgra, videoInfo.width, videoInfo.height, overlayImage, overlayText);
Для японского текста есть два момента.
- Если японские литералы пишете прямо в
.cpp, сохраняйте исходник как UTF-8 with BOM или собирайте MSVC с/utf-8. Иначе получите кракозябры. Если строка приходит аргументом командной строки,wmainпринимает её как UTF-16, и этой проблемы нет - Гарнитура в системе может отсутствовать. Как в коде выше, всегда готовьте запасной вариант. Если при отсутствии шрифта молча подставится другая гарнитура, причину съехавшей вёрстки потом трудно найти
flowchart TB
accTitle: Что менять, чтобы рисовать японский текст
accDescr: Чтобы вынести строку в аргумент и рисовать японский текст, добавляют константы имён шрифтов, аргумент в DrawOverlay, создание шрифта с запасным вариантом и приём аргумента в wmain; отдельно следят за кракозябрами и отсутствующим шрифтом.
jp1["Добавить константы имён шрифтов"] --> jp2["Добавить аргумент в DrawOverlay"]
jp2 --> jp3["Создавать шрифт с запасным вариантом"]
jp3 --> jp4["Принять аргумент в wmain и передать"]
jp3 -.-> jp5["Отсутствующий шрифт — на запасной"]
Рис. 19: Меняют четыре места; чаще всего подводят подготовка шрифта и кодировка.
10. Итоги
Когда нужно прожечь изображение или текст во все кадры MP4 средствами Media Foundation, картина становится яснее, если разложить задачу на четыре части.
- достать:
IMFSourceReader - нарисовать:
GDI+илиDirect2D / DirectWrite - привести к виду, который кодировщик принимает охотнее:
NV12и подобные - записать обратно:
IMFSinkWriter
А если нужен именно «пример, который целиком вставляется в один .cpp и сразу работает», схема вроде той, что использована здесь,
Source Reader -> RGB32 -> изображение + HelloWorld через GDI+ -> BGRA to NV12 -> Sink Writer
получается довольно прямой.
Дальше, доращивая до production, меньше ломается такая последовательность.
- Добавить remux звука
- Заменить
GDI+наDirect2D / DirectWrite - Увести преобразование в
NV12вVideo Processor MFTили на GPU - Для длинных роликов и высокого разрешения перейти на схему на
D3D11 surface - Если нужно переиспользование — вынести в custom
MFT
Если попытаться сделать всё сразу, разом навалятся COM, stride, цветовые пространства и управление поверхностями. И в проектировании, и в отладке гораздо проще сначала прогнать решение по этапам, а усиливать только те части, которым это действительно нужно.
flowchart TB
accTitle: Порядок, в котором доращивают до production
accDescr: Меньше ломается такой порядок: добавить remux звука, заменить GDI+ на Direct2D и DirectWrite, увести преобразование NV12 в Video Processor MFT или на GPU, для длинных роликов и высокого разрешения перейти на D3D11 surface и при необходимости вынести в custom MFT.
ex1["Добавить remux звука"] --> ex2["Заменить отрисовку на Direct2D"]
ex2 --> ex3["Преобразование — на MFT или GPU"]
ex3 --> ex4["Перейти на D3D11 surface"]
ex4 --> ex5["При необходимости вынести в custom MFT"]
Рис. 20: Не собирайте всё сразу — усиливайте по одному этапу в этом порядке.
11. Связанные статьи
- Введение в Media Foundation: как понять API через COM
- Как извлечь кадр из MP4 по заданному времени с помощью Media Foundation
12. Источники
- Полный набор примера кода к этой статье (однофайловый
.cppи конфигурация сборки CMake) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/media-foundation-overlay-image-text-on-mp4-frames - Microsoft Learn: Using the Source Reader to Process Media Data
- Microsoft Learn: MFCreateSourceReaderFromByteStream
- Microsoft Learn: MFCreateMFByteStreamOnStream
- Microsoft Learn: IMFSourceReader::SetCurrentMediaType
- Microsoft Learn: MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING
- Microsoft Learn: MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING
- Microsoft Learn: IMFSourceReader::ReadSample
- Microsoft Learn: Working with Media Samples
- Microsoft Learn: IMF2DBuffer::Lock2D
- Microsoft Learn: Video Subtype GUIDs
- Microsoft Learn: H.264 Video Encoder
- Microsoft Learn: Video Processor MFT
- Microsoft Learn: Using the Sink Writer
- Microsoft Learn: Tutorial: Using the Sink Writer to Encode Video
- Microsoft Learn: Interoperability Overview (Direct2D)
- Microsoft Learn: Text Rendering with Direct2D and DirectWrite
- Microsoft Learn: Writing a Custom MFT
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Как преобразовать YUV в RGB с помощью Media Foundation
Как в Media Foundation преобразовать кадр YUV в RGB: автоматическое преобразование Source Reader, самостоятельное преобразование NV12/YUY...
Как извлечь кадр из MP4 по заданному времени с помощью Media Foundation
Как Source Reader берёт из MP4 кадр, ближайший к заданному времени, выравнивает stride и alpha в RGB32 и сохраняет результат в PNG.
Введение в Media Foundation: как понять API через COM
Разбираем, что такое Media Foundation, вместе с базовой терминологией Windows-медиа-API — COM, HRESULT, IMFSourceReader, MFT — в том поря...
Разделяемая память: подводные камни и практические рекомендации
Подводные камни разделяемой памяти на практике и проектирование, которое снижает частоту сбоев: синхронизация, видимость, время жизни, AB...
Пул потоков Win32 — параллелизм через CreateThreadpoolWork без своих потоков
Не плодите ли вы CreateThread по всему нативному коду? Разбираем API пула потоков Win32, переработанный в Vista: четыре объекта work, tim...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Тема напрямую связана с реализацией Windows-приложений, где пересекаются Media Foundation, GDI+, Direct2D / DirectWrite, преобразование цвета и вывод видео.
Технические консультации и ревью дизайна
Подходит и для того, чтобы разложить, как вырастить однофайловую реализацию до схемы для production и где провести границу для remux звука или переноса вычислений на GPU.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Каков базовый порядок, чтобы прожечь изображение или текст в каждый кадр MP4 средствами Media Foundation?
- Базовая схема такая: «декодировать через Source Reader → наложить на несжатые кадры → при необходимости преобразовать цвет → перекодировать через Sink Writer». Само размещение изображения или текста — не задача Media Foundation, а задача API отрисовки вроде GDI+, Direct2D/DirectWrite или WIC. Для первой рабочей версии понятнее всего цепочка Source Reader → RGB32 → отрисовка через GDI+ → NV12 → Sink Writer; если важнее скорость и запас по расширению, имеет смысл смещаться к D3D11/DXGI surface и Direct2D/DirectWrite.
- Можно ли отдать кадры RGB32 в H.264-кодировщик как есть?
- Не факт. H.264-кодировщик Microsoft чаще всего рассчитан на вход семейства YUV — I420/IYUV/NV12/YUY2/YV12, поэтому после композитинга в удобных для рисования RGB32/ARGB32 почти всегда нужен этап преобразования. Либо вставляют Video Processor MFT и гонят RGB32 → NV12, либо пишут своё преобразование RGB → NV12. Кроме того, NV12 — это 4:2:0, поэтому ширина и высота кадра должны быть чётными.
- Для отрисовки наложения лучше взять GDI+ или Direct2D?
- В первой реализации GDI+ удобнее для однофайлового примера: загрузка изображений и рисование текста почти не требуют подготовки. На длинных роликах, в 4K и при массовой обработке связка D3D11 + Direct2D + DirectWrite часто выигрывает по производительности. Практичный путь — сначала прогнать весь конвейер на GDI+, а на этапе оптимизации скорости заменить отрисовку на Direct2D/DirectWrite и увести преобразование цвета в Video Processor MFT или на GPU: так схема не ломается и её проще расширять.
- На что смотреть, когда вызываете IMFSourceReader::ReadSample?
- ReadSample может вернуть S_OK, а sample при этом будет nullptr. Типичные случаи — потоковые события вроде MF_SOURCE_READERF_STREAMTICK и MF_SOURCE_READERF_ENDOFSTREAM. Поэтому в цикле проверяют сразу три вещи: HRESULT, flags и sample. Метка времени идёт в единицах по 100 нс; duration надёжнее наследовать из входного sample (timestamp и duration), чем каждый раз прибавлять константу из расчёта на фиксированный fps.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.