Comment incruster une image et du texte dans les images d'une vidéo MP4 avec Media Foundation
· Mis à jour le: · Go Komura · Media Foundation, C++, Développement Windows, GDI+, Direct2D, DirectWrite, H.264
Filigrane de logo, résultat d’inspection, numéro d’équipement, nom de l’opérateur, horodatage. L’exigence de créer un nouveau MP4 en incrustant ce type d’informations dans toutes les images d’une vidéo MP4 est assez courante dans la surveillance, l’inspection, la traçabilité et les interfaces d’analyse.
Cependant, dès qu’on commence à manipuler Media Foundation, on se retrouve face à IMFSourceReader, IMFSample, IMFMediaBuffer, IMFTransform, IMFSinkWriter, et il devient soudain difficile de voir à quel endroit, en fin de compte, superposer le texte ou le PNG.
Dans cet article, nous commençons par clarifier la vue d’ensemble — Source Reader -> dessin -> conversion de couleur -> Sink Writer — puis nous présentons un exemple tenant en un seul fichier, que vous pouvez coller directement dans une application console C++ de Visual Studio.
L’exemple lit le MP4 indiqué, dessine l’image indiquée ainsi que le texte HelloWorld sur chaque image, et produit un MP4 de sortie.
Notez que cet exemple privilégie le fait de pouvoir le coller et l’exécuter tel quel, et qu’il adopte donc une configuration qui ne réencode que la vidéo. On pourrait aussi intégrer le remuxage audio dans le même programme, mais comme le sujet de cet article est « incruster une image et du texte dans chaque image », nous nous concentrons d’abord sur ce point.
Le code présenté dans cet article est publié sur GitHub sous la forme d’un ensemble complet d’exemples (un .cpp autonome en un seul fichier, ainsi qu’une configuration de build CMake).
media-foundation-overlay-image-text-on-mp4-frames - komurasoft-blog-samples (GitHub)
1. La conclusion d’abord
- Le schéma de base pour placer une image ou du texte dans chaque image d’un MP4 est
décoder avec le Source Reader -> composer sur les images non compressées -> convertir la couleur si nécessaire -> réencoder avec le Sink Writer. - Le placement de l’image ou du texte en lui-même n’est pas le travail de Media Foundation. Il est plus naturel d’y réfléchir avec une API de dessin comme
GDI+,Direct2D,DirectWriteouWIC. - Pour revenir à
MP4(H.264), une étape de conversion reliant leRGB32 / ARGB32facile à dessiner et leNV12 / I420 / YUY2que l’encodeur accepte facilement devient souvent nécessaire. - Pour faire fonctionner une première version, la configuration
Source Reader -> RGB32 -> dessin avec GDI+ -> NV12 -> Sink Writerest facile à suivre. - Pour privilégier la vitesse et l’extensibilité, évoluer vers
D3D11 / DXGI surface -> Direct2D / DirectWrite -> Video Processor MFT -> Sink Writeroffre plus de marge de progression.
2. Pourquoi ce problème est un peu compliqué
« Mettre du texte dans une vidéo » mélange en réalité les quatre sujets suivants.
-
La question du conteneur et du codec
mp4est un conteneur, pas les images elles-mêmes. Le contenu est le plus souvent des données compressées enH.264ouH.265. -
La question du décodage / encodage Tant que les données restent compressées, on ne peut pas simplement superposer du texte ou un PNG avec une API de dessin 2D ordinaire. Il faut d’abord revenir à des images non compressées.
-
La question du dessin Le texte, les logos, la composition avec transparence d’un PNG et le rendu de texte anticrénelé ne relèvent pas de Media Foundation lui-même. C’est le travail de
GDI+ou deDirect2D / DirectWrite / WIC. -
La question de l’espace colorimétrique et du format de pixels Le format facile à dessiner et le format que préfère l’encodeur ne coïncident pas. C’est le point où l’on se retrouve discrètement bloqué.
En résumé en une phrase : plutôt que de penser « mettre du texte avec Media Foundation », il est plus clair de considérer que « Media Foundation fait circuler les images, une API de dessin les superpose, puis on applique la conversion de couleur nécessaire avant d’encoder ».
3. Le tableau récapitulatif à regarder en premier
| Approche | Configuration | Cas adaptés | Points d’attention |
|---|---|---|---|
| Faire d’abord fonctionner correctement | Source Reader -> RGB32 -> composition -> NV12 -> Sink Writer |
Traitement par lots, outils internes, implémentation initiale | Les copies et conversions côté CPU tendent à s’accumuler |
| Augmenter la vitesse | D3D11 / DXGI surface -> Direct2D / DirectWrite -> Video Processor MFT -> Sink Writer |
Vidéos longues, haute résolution, traitement en masse | La gestion de D3D11 et DXGI s’alourdit |
| En faire un composant réutilisable | Implémenter en tant que MFT personnalisé et l’insérer dans une topology |
Effet utilisé par plusieurs applications, intégration dans un pipeline MF | La difficulté d’implémentation, d’enregistrement et de débogage augmente |
L’exemple de cet article se limite à la première ligne, la configuration « faire d’abord fonctionner correctement ».
3.1 Vue d’ensemble du traitement
flowchart LR
A[input.mp4] --> B[IMFSourceReader]
B --> C[Image non compressée<br/>RGB32]
C --> D[Dessiner l'image + HelloWorld avec GDI+]
D --> E[Conversion BGRA -> NV12]
E --> F[IMFSinkWriter]
F --> G[output.mp4]
B --> H[Échantillons audio]
H --> I[Copier tel quel<br/>ou réencoder]
I --> F
Ce qui compte ici, c’est que le dessin en lui-même n’est pas le travail de Media Foundation. Media Foundation est chargé de faire entrer et sortir les images ; le placement de l’image et du texte est délégué à une API de dessin.
4. Comment décomposer le pipeline
4.1 Recevoir l’entrée avec IMFSourceReader
Si l’entrée est un chemin de fichier, utilisez MFCreateSourceReaderFromURL ; si ce sont des données vidéo en mémoire, il est plus clair de créer un IMFByteStream et d’utiliser MFCreateSourceReaderFromByteStream.
Ce qu’il faut décider en premier ici, c’est s’il faut recevoir les images dans un format facile à dessiner, ou dans un format destiné à l’encodeur.
- Pour simplifier l’implémentation,
RGB32ouARGB32 - Pour privilégier l’efficacité d’encodage, un format YUV tel que
NV12
Cela dit, la composition de texte ou de PNG est nettement plus simple à raisonner avec une famille RGB, donc recevoir les images en RGB32 / ARGB32 dès le départ est le choix le plus pratique.
Si vous activez MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, le Source Reader effectue la conversion YUV -> RGB32 ainsi que le désentrelacement pour vous.
C’est pratique au stade où l’on veut « d’abord extraire les images et les manipuler », mais cela tend à devenir coûteux avec des vidéos longues ou en haute résolution ; si vous avez besoin de vitesse en production, cela vaut la peine de revoir la configuration plus tard.
4.2 Penser la composition de l’image et du texte avec GDI+ ou Direct2D / DirectWrite
On extrait le tampon de l’IMFSample reçu de Media Foundation, puis on y place l’image du logo et le texte.
L’exemple de cet article privilégie le fait de tenir en un seul fichier facile à coller, et utilise donc GDI+ pour le dessin.
- Il permet de charger des images
- Il permet de dessiner du texte
- Il demande relativement peu de préparation supplémentaire
- Il s’intègre facilement dans un seul
.cppd’application console
En revanche, pour les usages traitant en masse des vidéos longues ou de la 4K, D3D11 + Direct2D + DirectWrite offre davantage de marge de progression.
Il est naturel d’utiliser GDI+ pour la première implémentation, puis de migrer vers Direct2D / DirectWrite au moment où il faut optimiser la vitesse.
4.3 On ne peut pas nécessairement écrire directement du RGB32 en H.264
C’est le point où l’on bute le plus souvent.
Lorsqu’on revient à MP4(H.264), l’encodeur H.264 de Microsoft suppose le plus souvent une entrée de type YUV telle que I420 / IYUV / NV12 / YUY2 / YV12.
Autrement dit, composer dans le RGB32 / ARGB32 facile à dessiner puis l’envoyer directement à IMFSinkWriter ne fonctionne pas forcément tel quel.
L’implémentation nécessite donc l’une des deux conversions suivantes.
- Insérer un
Video Processor MFTpour convertirRGB32 / ARGB32 -> NV12 - Implémenter soi-même la conversion
RGB -> NV12
L’exemple de cet article privilégie le fait de tenir en un seul fichier, et adopte donc la seconde option, la conversion faite maison.
En production, une configuration insérant un Video Processor MFT, capable de gérer ensemble la conversion d’espace colorimétrique, le redimensionnement et le désentrelacement, est aussi une option solide.
4.4 Écrire la sortie avec IMFSinkWriter
Pour la sortie vidéo, IMFSinkWriter est le plus facile à utiliser.
Le principe est simple :
- Le type de flux de sortie … le format que l’on veut écrire dans le fichier
Exemple :
MFVideoFormat_H264 - Le type de flux d’entrée … le format que l’application transmet au
Sink WriterExemple :MFVideoFormat_NV12
se configurent séparément.
Autrement dit, du point de vue du Sink Writer :
- l’application transmet des images non compressées en
NV12 - le
Sink Writerles encode en H.264 et les écrit dans le MP4
c’est la relation qui s’établit.
4.5 Traiter l’audio séparément dès le départ simplifie les choses
Il est très fréquent de vouloir seulement ajouter un logo ou du texte à la vidéo, sans toucher à l’audio lui-même.
En pratique, la configuration suivante
- le flux vidéo seul :
Source Reader -> composition -> Sink Writer - le flux audio : remuxé en restant compressé
est facile à utiliser.
Cependant, comme l’exemple de cet article se concentre sur l’incrustation d’une image et d’un texte dans les images, la sortie est un MP4 uniquement vidéo. Une version conservant l’audio sera plus facile à suivre si on l’ajoute plus tard, à l’étape d’extension.
5. Prérequis et utilisation de cet exemple
Les prérequis de ce code sont les suivants.
- Windows 10 / 11
- Une application console C++ Visual Studio 2022
- Build
x64 - Ce fichier
.cppn’utilise pas d’en-têtes précompilés - La largeur et la hauteur de la vidéo d’entrée sont paires
- L’entrée est un fichier vidéo MP4 ordinaire
- La sortie est un MP4 uniquement vidéo
- L’image est dans un format que GDI+ peut lire, tel que PNG / JPEG / BMP / GIF
NV12 étant en 4:2:0, la largeur et la hauteur doivent être paires.
C’est pourquoi cet exemple lève explicitement une erreur lorsque cette condition n’est pas remplie.
5.1 Utilisation
- Créer une Console App dans Visual Studio
- Coller ce
.cppen entier - Régler ce fichier
.cppsur « Ne pas utiliser » les en-têtes précompilés - Compiler en
x64 - L’exécuter comme suit
OverlayMp4.exe input.mp4 overlay.png output.mp4
input.mp4La vidéo sourceoverlay.pngL’image à superposeroutput.mp4La destination de sortie
La chaîne de caractères est fixée à HelloWorld dans kOverlayText, en haut du code.
La position et la taille peuvent aussi être modifiées en ajustant les constantes du code.
6. Code autonome en un seul fichier, à coller directement dans un .cpp
#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. Points à retenir en lisant cette implémentation
7.1 Le format facile à dessiner et le format accepté par l’encodeur sont différents
Cet exemple suit le flux suivant.
- Sortie du
Source Reader:RGB32 - Dessin :
GDI+ - Entrée du
Sink Writer:NV12
La raison est simple : les formats RGB sont faciles à utiliser pour superposer du texte ou un PNG, et NV12 est facile à transmettre à l’encodage H.264.
En lisant l’implémentation, il devient plus facile de suivre si l’on distingue « l’étape de dessin » et « l’étape de mise en forme avant l’encodage ».
7.2 Le stride et l’orientation verticale sont normalisés avant le dessin
Les images vidéo ne sont pas nécessairement disposées en mémoire de la même façon qu’elles apparaissent à l’écran.
- Le stride peut ne pas correspondre à
width * 4 - L’orientation verticale peut être inversée
IMF2DBufferetIMFMediaBufferse manipulent de façon légèrement différente
C’est pourquoi ce code normalise d’abord les données vers un tampon BGRA orienté de haut en bas avant de dessiner. Régler ce point en amont permet de garder le code de dessin nettement plus simple.
7.3 Avec ReadSample, vérifier les flags et le sample, pas seulement le HRESULT
ReadSample peut retourner S_OK alors que sample == nullptr.
Les cas typiques sont :
MF_SOURCE_READERF_STREAMTICKMF_SOURCE_READERF_ENDOFSTREAM- d’autres événements de flux
La boucle doit donc examiner ensemble ces trois éléments : le HRESULT, les flags et l’inputSample.
En particulier, si l’on néglige STREAMTICK et ENDOFSTREAM, le traitement de la timeline en aval a tendance à se dérégler.
7.4 Il est plus sûr de reprendre le timestamp et la duration de l’entrée
L’horodatage est exprimé en unités de 100 ns.
De plus, la duration doit être récupérée séparément depuis l’IMFSample.
Plutôt que d’ajouter à chaque fois une valeur fixe calculée sur l’hypothèse d’un fps constant, il est plus robuste de reprendre autant que possible le timestamp / la duration du sample d’entrée.
Cet exemple aussi ne se rabat sur une valeur par défaut calculée à partir du fps que lorsque la duration ne peut pas être obtenue.
7.5 GDI+ est léger à mettre en place, mais il y a une étape suivante pour les vidéos longues ou en haute résolution
GDI+ convient très bien à un exemple tenant en un seul fichier, mais pour les usages traitant en masse des vidéos longues ou de la 4K, D3D11 + Direct2D + DirectWrite peut avoir l’avantage.
- D’abord faire fonctionner l’ensemble avec
GDI+ - Ensuite, remplacer par
Direct2D / DirectWritesi nécessaire - Déplacer la conversion de couleur vers un
Video Processor MFTou le GPU
Une progression par étapes de ce type permet d’étendre le système sans casser la conception.
7.6 Cet exemple se limite à la vidéo
Si l’on entassait aussi l’audio dans le même article, le fil du propos risquerait de se disperser. C’est pourquoi cet exemple se concentre sur l’incrustation d’une image et d’un texte dans les images vidéo, et la sortie est un MP4 uniquement vidéo.
En pratique, l’étape suivante consisterait à évoluer vers
- vidéo seule :
Source Reader -> composition -> Sink Writer - audio : remuxé en restant compressé
ce qui est une configuration facile à gérer.
8. Si les « données vidéo fournies » sont une suite d’octets MP4 en mémoire plutôt qu’un fichier
Le code de cet article utilise MFCreateSourceReaderFromURL, donc l’entrée est un chemin de fichier.
Cependant, si l’exigence est « faire la même chose sur une suite d’octets mp4 reçue d’une API », le raisonnement ne change pas. Seul le point d’entrée change.
- Préparer un
IStreamou un flux personnalisé - Le transmettre au
Source Readersous la forme d’unIMFByteStream - Ensuite, c’est le même schéma :
RGB32 -> dessin -> NV12 -> Sink Writer
L’essentiel réside donc, non pas dans la façon dont les données vidéo sont conservées, mais dans la manière de dessiner sur chaque image après décodage.
9. Pour faire évoluer vers la production
9.1 Ajouter le remuxage audio
L’extension la plus pratique à envisager en premier est de conserver l’audio tel quel. En ne réencodant que la vidéo et en réécrivant l’audio dans le même format tout en restant compressé, on répond à l’exigence sans alourdir considérablement l’implémentation.
9.2 Insérer un Video Processor MFT
L’exemple de cet article privilégie le fait de tenir en un seul fichier et convertit BGRA -> NV12 par ses propres moyens, mais en production, une configuration insérant un Video Processor MFT est aussi une option assez solide.
Avec Video Processor MFT, il devient plus facile de gérer ensemble :
- la conversion d’espace colorimétrique
- le redimensionnement
- le désentrelacement
- la conversion de fréquence d’images
9.3 Remplacer GDI+ par Direct2D / DirectWrite
Pour des incrustations comme une image de logo, des sous-titres ou un horodatage, GDI+ suffit souvent, mais si l’on veut optimiser la performance, Direct2D / DirectWrite a l’avantage.
En particulier, si l’on se trouve dans des conditions telles que
- haute résolution
- longue durée
- grand nombre de vidéos
- volonté future de basculer vers un chemin GPU
alors une configuration utilisant D3D11 / DXGI surface entre en ligne de compte.
9.4 Envisager un MFT personnalisé lorsque cela devient « un effet vidéo que l’on veut réutiliser »
Dans Media Foundation, un effet peut être implémenté sous la forme d’un IMFTransform.
Si l’on souhaite donc réutiliser le même traitement d’incrustation dans plusieurs applications ou pipelines, un MFT personnalisé est un choix élégant.
Cependant, pour une première implémentation,
- il faut satisfaire le contrat
IMFTransform - la gestion des types de média en entrée et en sortie s’alourdit
- la difficulté d’enregistrement et de débogage augmente
c’est pourquoi, en pratique, il est souvent plus simple de d’abord faire fonctionner correctement Source Reader + composition + Sink Writer, puis d’extraire un MFT seulement quand cela devient nécessaire.
10. Résumé
Lorsqu’on incruste une image ou du texte dans toutes les images d’une vidéo MP4 avec Media Foundation, décomposer le problème en ces quatre points permet d’y voir plus clair.
- Extraire :
IMFSourceReader - Dessiner :
GDI+ouDirect2D / DirectWrite - Convertir vers un format accepté par l’encodeur :
NV12, etc. - Réécrire :
IMFSinkWriter
Et si l’on veut « un exemple que l’on colle entièrement dans un seul .cpp et qui fonctionne tel quel », une configuration comme celle de cet article,
Source Reader -> RGB32 -> image + HelloWorld avec GDI+ -> BGRA vers NV12 -> Sink Writer
est tout à fait naturelle.
Si l’on veut ensuite faire évoluer cela pour la production, réfléchir dans cet ordre évite que les choses ne se déstructurent.
- Ajouter le remuxage audio
- Remplacer
GDI+parDirect2D / DirectWrite - Déplacer la conversion
NV12vers unVideo Processor MFTou le GPU - Passer à une base
D3D11 surfacepour les contenus longs et en haute résolution - Extraire un
MFTpersonnalisé si la réutilisabilité est nécessaire
Si l’on tente de tout faire d’un coup, COM, le stride, l’espace colorimétrique et la gestion des surfaces arrivent tous en même temps. Faire fonctionner les choses étape par étape d’abord, puis ne renforcer que les points nécessaires ensuite, rend la conception comme le débogage nettement plus faciles.
11. Articles connexes
- Qu’est-ce que Media Foundation ? Pourquoi le visage COM de l’API multimédia Windows finit par prendre sens
- Comment extraire une image fixe d’un MP4 à un instant précis avec Media Foundation - Version autonome en un seul fichier à coller directement dans un .cpp
12. Références
- L’ensemble du code d’exemple de cet article (un
.cppautonome en un seul fichier, ainsi qu’une configuration de build 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
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Comment convertir du YUV en RGB avec Media Foundation
Comment convertir des trames YUV en RGB avec Media Foundation : la conversion automatique du Source Reader vers RGB32, ou la conversion m...
Extraire une image fixe d'un MP4 à un instant précis avec Media Foundation
Comment récupérer, avec le Source Reader, la frame la plus proche d'un instant donné dans un MP4, corriger le stride et l'octet alpha du ...
Introduction à Media Foundation - Comprendre l'API à travers le prisme COM
Nous expliquons ce qu'est Media Foundation, ainsi que le vocabulaire de base des API multimédias Windows - COM, HRESULT, IMFSourceReader,...
Pièges de la mémoire partagée et bonnes pratiques concrètes
Nous passons en revue les pièges de la mémoire partagée en usage réel, ainsi que la conception qui réduit le taux d'incidents : synchroni...
Appeler une DLL C# Native AOT depuis C/C++
Nous faisons le point sur la publication d'une bibliothèque de classes C# en DLL native avec Native AOT, et sur l'appel depuis C/C++ des ...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Ce sujet est directement lié au développement d'applications Windows qui associe Media Foundation, GDI+, Direct2D / DirectWrite, la conversion de couleur et la sortie vidéo.
Conseil technique et revue de conception
Il se prête aussi à une réflexion de conception sur la façon de faire évoluer une implémentation en un seul fichier vers une architecture de production, et sur l'endroit où placer la limite entre le remuxage audio et le passage au GPU.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Quel est le flux de base pour incruster une image ou du texte dans chaque image d'un MP4 avec Media Foundation ?
- Le schéma de base est : « décoder avec le Source Reader -> composer sur les images non compressées -> convertir la couleur si nécessaire -> réencoder avec le Sink Writer ». Le placement de l'image ou du texte en lui-même n'est pas le travail de Media Foundation, mais celui d'une API de dessin comme GDI+, Direct2D/DirectWrite ou WIC. Pour faire fonctionner une première version, la configuration Source Reader -> RGB32 -> dessin avec GDI+ -> NV12 -> Sink Writer est facile à suivre ; si vous privilégiez la vitesse et l'extensibilité, vous gagnez en marge de manœuvre en évoluant vers une configuration utilisant une surface D3D11/DXGI avec Direct2D/DirectWrite.
- Peut-on transmettre directement une image RGB32 à l'encodeur H.264 ?
- Pas nécessairement. L'encodeur H.264 de Microsoft suppose le plus souvent une entrée de type YUV telle que I420/IYUV/NV12/YUY2/YV12, si bien qu'après avoir composé l'image dans le RGB32/ARGB32 facile à dessiner, une étape de conversion devient généralement nécessaire. On insère alors un Video Processor MFT pour convertir de RGB32 vers NV12, ou l'on implémente soi-même la conversion RGB vers NV12. De plus, comme NV12 est en 4:2:0, la largeur et la hauteur de l'image doivent être paires.
- Faut-il utiliser GDI+ ou Direct2D pour dessiner l'incrustation ?
- Pour une première implémentation, GDI+, qui permet de charger des images et de dessiner du texte avec peu de préparation supplémentaire, convient bien à un exemple tenant en un seul fichier. En revanche, pour les vidéos longues, la 4K ou le traitement en masse, D3D11 + Direct2D + DirectWrite peut avoir l'avantage en performance. Une progression par étapes — faire fonctionner l'ensemble avec GDI+ d'abord, puis passer à Direct2D/DirectWrite quand il faut gagner en vitesse, et déplacer la conversion de couleur vers un Video Processor MFT ou le GPU — permet d'étendre le système sans casser la conception.
- À quoi faut-il faire attention en utilisant ReadSample de IMFSourceReader ?
- ReadSample peut retourner S_OK alors que sample vaut nullptr. Les cas typiques sont les événements de flux tels que MF_SOURCE_READERF_STREAMTICK ou MF_SOURCE_READERF_ENDOFSTREAM. C'est pourquoi la boucle doit vérifier ensemble les trois éléments : le HRESULT, les flags et le sample. Par ailleurs, l'horodatage est exprimé en unités de 100 ns, et il est plus robuste de reprendre autant que possible le timestamp et la duration de l'échantillon d'entrée plutôt que d'ajouter une valeur fixe calculée à partir d'un fps supposé constant.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics