Rendu optimisé des flux vidéo avec OpenGL
Cet article vous propose une analyse technique sur la manière dont la vidéo d’un appel VoIP est rendue à l’écran. Bien que cela puisse sembler être une opération assez basique à première vue, il n’en est rien, car cela nécessite des calculs très intensifs pour le processeur afin de convertir le format des pixels. En déléguant ces calculs à la carte graphique, nous économisons une grande partie de la bande passante du processeur, qui peut alors être allouée à l’encodeur vidéo, permettant ainsi d’obtenir une meilleure qualité globale.
Notre développeur Thibault, qui travaillait auparavant sur ce projet au sein de l’équipe Liblinphone et fait désormais partie de l’équipe Flexisip, vous a rédigé l’explication suivante.
Le récit d’un développeur
OpenGL, espaces colorimétriques et Linphone
Qu’est-ce qu’OpenGL ? Qu’est-ce que Y’CbCr ? Et pourquoi Linphone a-t-il besoin de tout cela ?
Dans cet article, nous allons nous concentrer sur la toute dernière étape de cette chaîne : le moteur de rendu vidéo.
Le moteur de rendu reçoit en entrée les données du décodeur vidéo (H.264, VP8, etc.) et les dessine dans une mémoire graphique, qui correspond au rectangle que vous voyez sur votre écran (celui avec le visage souriant de votre ami).
C’est ici que nous rencontrons Y’CbCr et les espaces colorimétriques. Pour des raisons techniques, la sortie d’un décodeur vidéo est une image dans un espace colorimétrique Y’CbCr.
La plupart des programmeurs et des artistes numériques ont l’habitude de représenter les images dans l’espace RGB, qui est une grille de pixels avec trois canaux : rouge, vert et bleu (et parfois un canal alpha pour la transparence).
Un cadre Y’CbCr est similaire, sauf qu’il encode la couleur avec trois canaux différents : luma, chroma bleu et chroma rouge.
À ce stade, et pour plus de détails sur le fonctionnement de cet espace colorimétrique, je vous invite à lire le résumé et la section Rationale de l’article Wikipédia consacré à Y’CbCr.
Tout ce qu’il reste alors à faire au moteur de rendu, c’est de traduire l’image en RGB, l’adapter à la résolution de la mémoire graphique de sortie, et dessiner le résultat dans ce buffer.
Cependant, c’est un processus massivement parallèle, car il faut le faire pour chaque pixel. Si vous avez des connaissances en graphisme informatique, vous savez où je veux en venir : c’est un travail pour votre carte graphique !
Pour pouvoir dialoguer avec la carte graphique, il faut utiliser OpenGL.
OpenGL (Open Graphics Library) est une bibliothèque et un standard multiplateforme qui permet au programmeur de piloter un GPU (graphics processing unit) pour exécuter des tâches comme le rendu 2D et 3D.
Bien connue dans l’industrie du jeu vidéo, au même titre que DirectX et Metal (des bibliothèques équivalentes mais non multiplateformes), OpenGL est aujourd’hui en passe d’être remplacée par le nouveau standard Vulkan.
Voilà pourquoi Linphone utilise OpenGL pour effectuer la conversion Y’CbCr → RGB lors du rendu vidéo.
Le contexte de ma mission autour du moteur de rendu vidéo était de faire évoluer le code existant pour le rendre conforme à la spécification OpenGL 4.1. Il y avait un code existant que je devais faire évoluer ou réécrire, mais en tout cas, je pensais bien comprendre ce qu’il faisait…
J’ai découvert que le code faisait la conversion Y’CbCr en RGB plus ou moins ainsi :
Il s’agit en gros d’une matrice 3×3 appliquée à chaque pixel Y’CbCr pour obtenir un pixel RGB.
Une image en 720p contient 921 600 pixels, ce qui représente un volume de calcul important, et c’est là qu’OpenGL intervient.
En version 2.0 (2004), OpenGL a introduit le concept de shaders via le OpenGL Shading Language, un langage proche du C permettant d’exprimer les calculs exécutés par le GPU.
Conçus à l’origine pour calculer la lumière et la couleur lors du rendu d’objets 3D, les shaders se sont diversifiés pour exécuter de nombreuses fonctions spécialisées.
Ici, un usage détourné mais malin des shaders est appliqué : il n’y a pas de véritable scène 3D, on dessine simplement une texture plane à l’écran. Cette texture, au lieu d’être un buffer RGB comme attendu, est une image Y’CbCr, et le programme du shader est exécuté sur le GPU pour transformer cette texture Y’CbCr en RGB. Simple, non ?
Mais d’où vient cette matrice de transformation ?
J’ai cherché « yuv2rgb » sur le web, ainsi que certains des nombres « magiques » utilisés, comme 1.164.
Bien que j’aie trouvé de nombreux exemples de code (principalement en C) utilisant des constantes similaires pour « convertir YUV en RGB » (quoi que cela signifie), aucun n’expliquait comment ces constantes avaient été obtenues.
Refusant d’abandonner, j’ai fini par tomber sur l’article Wikipédia sur YUV qui — au-delà des chiffres et des formules qui ne semblaient pas correspondre aux nombres de mon shader — m’a conduit à l’article sur Y’CbCr.
Là, tout a enfin commencé à avoir du sens. J’ai découvert que ces nombres dits « magiques » étaient en fait issus de la simplification (au sens mathématique) de la matrice de conversion inverse du standard ITU-R BT.601, mélangée à des décalages et des mises à l’échelle en plage partielle. Notamment, le facteur 1.164 mentionné précédemment est une version arrondie du facteur d’échelle 255/219 que l’on retrouve dans la section ITU-R BT.601 conversion de cet article.
Je sais, je sais — je ne comprends pas la moitié de tout ça non plus — mais l’important, c’est que mes nombres sont passés de magiques à scientifiques.
Je n’ai pas besoin de comprendre la physique de l’émission lumineuse des phosphores, ni comment ont été choisis Kr, Kg et Kb. Je dois juste savoir comment ils ont été choisis.
Exemple de code avec documentation
Après toutes mes recherches et efforts, voici le code final que nous utilisons aujourd’hui dans le contexte OpenGL 4.1 :
J’ai essayé de le concevoir de manière à ce qu’il soit facile à suivre avec l’article Wikipédia ouvert à côté, et j’ai utilisé des noms explicites.
Conclusion
Déléguer certaines tâches au GPU est souvent une bonne idée pour économiser du temps processeur et de l’énergie.
Cette conversion d’espace colorimétrique, sous forme non optimisée (code C pur sans instructions assembleur SIMD), consommerait à elle seule autant que l’ensemble du processus d’encodage vidéo sur le processeur principal. Cela montre à quel point cette optimisation est importante dans la chaîne de traitement du flux vidéo !
La nouvelle API Vulkan, un standard ouvert pour les graphismes 3D, a annoncé des API de codec vidéo accélérées par GPU (H.264, H.265, AV1). Cela semble extrêmement prometteur pour optimiser encore davantage la chaîne de traitement vidéo de Linphone en déléguant des tâches supplémentaires au GPU.
Pour toute information complémentaire, n’hésitez pas à contacter notre équipe !