Énoncé du problème:
Le matériel Intel MFT n'honore pas le paramètre GOP, ce qui entraîne une consommation de bande passante accrue dans les applications en temps réel. Le même code fonctionne très bien sur le matériel Nvidia MFT.
Contexte:
J'essaie d'encoder des échantillons NV12 capturés via les API DesktopDuplication en flux vidéo à l'aide de l'encodeur matériel MediaFoundation H264 sur une machine Windows10, de les diffuser et de les rendre en temps réel sur un réseau local.
Au départ, je faisais face à trop de tampon au niveau de l'encodeur car l'encodeur tamponnait jusqu'à 25 images (taille GOP) avant de fournir un échantillon de sortie. Après quelques recherches, j'ai compris que la définition du CODECAPI_AVLowLatencyMode réduirait la latence avec le coût d'un peu de qualité et de bande passante.
La définition de la propriété CODECAPI_AVLowLatencyMode a quelque peu amélioré les performances, mais pas à la hauteur des exigences en temps réel. Il semble maintenant que l'encodeur tamponne encore jusqu'à 15 images au moins avant de produire les échantillons (introduisant un retard d'environ 2 secondes dans la sortie). Et ce comportement n'est visible que lorsqu'une faible fréquence d'images est configurée. À 60 images par seconde, la sortie est presque en temps réel sans retard visuellement perceptible.
En fait, la mise en mémoire tampon n'est perceptible à l'œil humain que lorsque la fréquence d'images est inférieure à 30 images par seconde. Et, le retard augmente inversement proportionnellement à la configuration FPS, à 25FPS le retard est de quelques centaines de millisecondes et monte jusqu'à 3 secondes lorsque FPS est configuré à 10 (taux constant). Je suppose que le réglage de FPS sur plus de 30 (disons 60FPS) fait en fait déborder le tampon de l'encodeur assez rapidement pour produire des échantillons avec un retard imperceptible.
Dernièrement, j'ai essayé la propriété CODECAPI_AVEncCommonRealTime ( https://docs.Microsoft.com/en-us/windows/win32/directshow/avenccommonrealtime-property ) ainsi que pour vérifier si elle améliore les performances lors de la réduction de la taux de trame d'entrée pour éviter la consommation de bande passante, mais cet appel échoue avec erreur "paramètre incorrect" .
Mes expériences:
Pour maintenir une fréquence d'images constante, et aussi pour forcer l'encodeur à produire des sorties en temps réel, j'alimente le même échantillon (échantillon précédemment enregistré) à l'encodeur à une vitesse constante de 30FPS/60FPS. Je fais cela en capturant seulement au plus 10FPS (ou à n'importe quel FPS requis) et en simulant 30/60FPS en alimentant le même échantillon trois fois ou exactement à un taux basé sur le ratio EMULATED_FRAME_RATE/ACTUAL_FRAME_RATE (Ex: 30/10, 60/15 , 60/20) pour combler l'écart exactement à intervalles constants. Par exemple, quand aucun changement ne se produit pendant 10 secondes, j'aurais alimenté l'encodeur avec le même échantillon 30 * 10 fois (30FPS). J'ai appris cette approche à partir de certains projets Github open source, également à partir d'échantillons de code expérimental de chrome, j'ai également été informé ( principalement sur SO, et également sur d'autres forums) que c'est la seule façon de pousser l'encodeur pour une sortie en temps réel, et il n'y a aucun moyen de contourner cela.
L'approche mentionnée ci-dessus produit une sortie en temps quasi réel mais consomme plus de données que ce à quoi je m'attendais même si je ne fournis que l'échantillon précédemment enregistré à l'encodeur.
Le débit de sortie semble rester constamment entre 350 Ko et 500 Ko sur Intel MFT, et varie entre 80 Ko et 400 Ko sur NVidia MFT (avec une configuration de 30 FPS et 500 Ko), peu importe si le contenu de l'écran change à 30 FPS ou 0 FPS (inactif). L'encodeur matériel NVidia semble être un peu mieux dans ce cas.
En fait, pendant le temps d'inactivité de l'écran, l'encodeur produisait beaucoup plus de données par seconde que le débit mentionné ci-dessus. J'ai été en mesure de réduire la consommation de données sur les appareils NVidia en définissant une taille GOP plus grande (la taille GOP actuelle configurée est de 16K). Mais tout de même, la consommation de données au ralenti de l'écran reste autour de 300 Ko/s sur le matériel Intel Graphics 620, et de 50 Ko/s à 80 Ko/s sur NVidia GTX 1070 (configuration: débit de 500 Ko et 30 images/seconde), ce qui est inacceptable. Je suppose que le matériel Intel MFT n'honore pas du tout le paramètre GOP ou l'amélioration est imperceptible.
J'ai également pu réduire la consommation de données en veille à ~ 130 Ko et ~ 40 Ko sur le matériel Intel et Nvidia respectivement en définissant des débits très faibles, mais cela est toujours inacceptable, cela détériore également la qualité vidéo.
Existe-t-il un moyen de configurer l'encodeur pour produire une sortie inférieure à ~ 10 Ko/s en l'absence de changement entre les échantillons d'entrée? J'ai en fait visé la sortie ~ 0KB quand aucun changement ne se produit mais ~ 10KBps est quelque peu acceptable.
Mise à jour:
Je suis en mesure de réduire la consommation de données d'inactivité sur NVidia MFT en ajustant certains paramètres à moins de ~ 20 Ko/s avec une configuration de débit de 400 Ko , et en dessous ~ 10 Ko/s avec une configuration de débit de 100 Ko . C'est convaincant. Mais le même code avec les mêmes configurations d'encodeur produit 20 à 40 fois plus de données sur les machines Intel. Intel (Intel Graphics 620) ne respecte certainement pas le paramètre GOP. J'ai même essayé de faire varier le GOP entre 256 et INT_MAX, rien ne semble changer sur la sortie du matériel Intel MFT.
Mise à jour 2:
Après avoir joué avec les propriétés de l'encodeur (j'ai seulement configuré CODECAPI_AVEncCommonRateControlMode avec eAVEncCommonRateControlMode_UnconstrainedVBR au lieu de eAVEncCommonRateControlMode_CBR), maintenant je pouvais voir que l'Intel MFT produisait 3 secondes seulement pendant quelques secondes) , puis cela revient à la même histoire. Je suppose qu'après quelques secondes, l'encodeur perd la référence à l'image clé à laquelle il compare les échantillons et il semble ne pas récupérer après ce point. Le comportement est le même, que le GOP soit 16/128/256/512/1024 ou INT_MAX.
Configurations de l'encodeur:
Référence: http://alax.info/blog/1586
const int EMULATED_FRAME_RATE = 30;//
const int TARGET_FPS = 10;
const int FPS_DENOMINATOR = 1;
const unsigned long long time_between_capture = 1000 / TARGET_FPS;
const unsigned long long nEmulatedWaitTime = 1000 / EMULATED_FRAME_RATE;
const unsigned long long TARGET_AVERAGE_BIT_RATE = 4000000; // Adjusting this affects the quality of the H264 bit stream.
const LONGLONG VIDEO_FRAME_DURATION = 10ll * 1000ll * 1000ll / ((long long)EMULATED_FRAME_RATE); // frame duration in 100ns units
const UINT32 KEY_FRAME_SPACING = 16384;
const UINT32 GOP_SIZE = 16384;
const UINT32 BPICTURECOUNT = 2;
VARIANT var = { 0 };
//no failure on both Nvidia & Intel, but Intel seems to be not behaving as expected
var.vt = VT_UI4;
var.lVal = GOP_SIZE;
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVEncMPVGOPSize, &var), "Failed to set GOP size");
var.vt = VT_BOOL;
var.ulVal = VARIANT_TRUE;
// fails with "parameter incorrect" error.
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVEncCommonRealTime, &var), "Failed to set realtime mode");
var = { 0 };
var.vt = VT_BOOL;
var.ulVal = VARIANT_TRUE;
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVLowLatencyMode, &var), "Failed to set low latency mode");
var = { 0 };
var.vt = VT_BOOL;
var.ulVal = VARIANT_TRUE;
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVEncCommonLowLatency, &var), "Failed to set low latency mode");
var = { 0 };
var.vt = VT_UI4;
var.lVal = 2; // setting B-picture count to 0 to avoid latency and buffering at both encoder and decoder
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVEncMPVDefaultBPictureCount, &var), "Failed to set B-Picture count");
var = { 0 };
var.vt = VT_UI4;
var.lVal = 100; //0 - 100 (100 for best quality, 0 for low delay)
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVEncCommonQualityVsSpeed, &var), "Failed to set Quality-speed ratio");
var = { 0 };
var.vt = VT_UI4;
var.lVal = 20;
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVEncCommonQuality, &var), "Failed to set picture quality");
var = { 0 };
var.vt = VT_UI4;
var.lVal = eAVEncCommonRateControlMode_CBR; // This too fails on some hardware
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVEncCommonRateControlMode, &var), "Failed to set rate control");
var = { 0 };
var.vt = VT_UI4;
var.lVal = 4000000;
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVEncCommonMeanBitRate, &var), "Failed to set Adaptive mode");
var = { 0 };
var.vt = VT_UI4;
var.lVal = eAVEncAdaptiveMode_FrameRate;
CHECK_HR(mpCodecAPI->SetValue(&CODECAPI_AVEncAdaptiveMode, &var), "Failed to set Adaptive mode");
J'ai essayé de récupérer la plage de paramètres prise en charge pour la taille du GOP avec le code suivant, mais cela renvoie simplement une erreur E_NOTIMPL.
VARIANT ValueMin = { 0 };
VARIANT ValueMax = { 0 };
VARIANT SteppingDelt = { 0 };
HRESULT hr = S_OK;
if (!mpCodecAPI) {
CHECK_HR(_pTransform->QueryInterface(IID_PPV_ARGS(&mpCodecAPI)), "Failed to get codec api");
}
hr = mpCodecAPI->GetParameterRange(&CODECAPI_AVEncMPVGOPSize, &ValueMin, &ValueMax, &SteppingDelt);
CHECK_HR(hr, "Failed to get GOP range");
VariantClear(&ValueMin);
VariantClear(&ValueMax);
VariantClear(&SteppingDelt);
Suis-je en train de manquer quelque chose? Existe-t-il d'autres propriétés que je pourrais expérimenter pour obtenir des performances en temps réel tout en consommant le moins de bande passante possible en l'absence de changement de contenu d'écran?
Un miracle s'est produit. Tout en jouant avec les configurations d'encodeur, j'ai accidentellement changé mon moniteur principal pour un autre sur ma machine, maintenant le problème a disparu. Le retour au moniteur principal précédemment sélectionné entraîne le même problème. Je soupçonne le d3ddevice d'être le fauteur de troubles. Je ne sais pas encore pourquoi cela se produit uniquement sur cet appareil/moniteur, je dois en expérimenter davantage.
Remarque: Je ne marque pas cela comme une réponse car je ne connais pas encore la raison du problème sur ce moniteur/périphérique d3d. Il suffit de poster cela comme référence pour d'autres personnes qui peuvent rencontrer une situation similaire. Je mettrai à jour la réponse une fois que je pourrai trouver la raison du comportement étrange sur cette instance particulière de d3d11device.
C'est ainsi que je crée le d3ddevice et que je le réutilise pour la capture d'image de duplication de bureau, le processeur vidéo pour la conversion des couleurs et également pour la transformation matérielle via la propriété MFT_MESSAGE_SET_D3D_MANAGER.
Options:
const D3D_DRIVER_TYPE m_DriverTypes[] = {
//Hardware based Rasterizer
D3D_DRIVER_TYPE_HARDWARE,
//High performance Software Rasterizer
D3D_DRIVER_TYPE_WARP,
//Software Rasterizer (Low performance but more accurate)
D3D_DRIVER_TYPE_REFERENCE,
//TODO: Explore other driver types
};
const D3D_FEATURE_LEVEL m_FeatureLevel[] = {
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_2,
D3D_FEATURE_LEVEL_9_1
//TODO: Explore other features levels as well
};
int m_DriversCount = ARRAYSIZE(m_DriverTypes);
int m_FeatureLevelsCount = ARRAYSIZE(m_FeatureLevel);
Créez d3ddevice:
DWORD errorCode = ERROR_SUCCESS;
if (m_FnD3D11CreateDevice == NULL)
{
errorCode = loadD3D11FunctionsFromDll();
}
if (m_Id3d11Device)
{
m_Id3d11Device = NULL;
m_Id3d11DeviceContext = NULL;
}
UINT uiD3D11CreateFlag = (0 * D3D11_CREATE_DEVICE_SINGLETHREADED) | D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
if (errorCode == ERROR_SUCCESS)
{
if (m_FnD3D11CreateDevice) {
for (UINT driverTypeIndex = 0; driverTypeIndex < m_DriversCount; ++driverTypeIndex)
{
m_LastErrorCode = D3D11CreateDevice(nullptr, m_DriverTypes[driverTypeIndex], nullptr, uiD3D11CreateFlag,
m_FeatureLevel, m_FeatureLevelsCount, D3D11_SDK_VERSION, &m_Id3d11Device, &m_SelectedFeatureLevel, &m_Id3d11DeviceContext);
if (SUCCEEDED(m_LastErrorCode))
{
break;
}
}
}
}