L'optimisation des flux vidéo grâce à OpenGL

Primary tabs

Cet article est un aperçu technique de la manière dont la vidéo d'un appel VoIP s'affiche à l'écran. Bien que cette opération puisse sembler assez basique à première vue, il n'en est rien : elle nécessite des calculs de conversion de format de pixels très gourmands en ressources processeur. En déchargeant ces calculs sur la carte graphique, nous pouvons économiser une grande partie de la bande passante de l'unité centrale, qui peut alors être allouée à l'encodeur vidéo, ce qui permet d'obtenir une meilleure qualité d'ensemble. Notre développeur Thibault, qui a travaillé sur ce projet au sein de l'équipe Liblinphone et qui fait désormais partie de l'équipe Flexisip, a rédigé les explications suivantes pour vous :

A developer's story time

Qu'est-ce qu'OpenGL ? Qu'est-ce que Y'CbCr ? Et pourquoi Linphone a besoin de tout cela ?

Dans cet article, nous nous concentrerons sur la toute dernière partie de cette pipeline : Le moteur de rendu vidéo.

Le moteur de rendu est alimenté par le décodeur vidéo (H.264, VP8, etc.) et peint sa sortie dans un buffer graphique, qui est le rectangle que vous voyez sur votre écran (celui où il y a votre ami.e qui vous sourit).

C'est ici que nous rencontrons Y'CbCr et les espaces colorimétriques. Pour des raisons techniques, c'est ce que les décodeurs vidéo produisent : une image dans un espace colorimétrique Y'CbCr.
La plupart des programmeurs et des graphistes sont habitués à représenter des images dans l'espace colorimétrique RGB : une grille de pixels avec trois canaux, le rouge (R), le vert (Green) et le bleu (B) (et parfois un canal Alpha pour la transparence).
Une trame de données Y'CbCr est presque identique, sauf qu'elle code 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 encourage à lire cet article Wikipédia sur Y'CbCr, en particulier la section Avantages et principes techniques.

Tout ce qu'il reste à faire pour un moteur de rendu est de traduire la trame de données dans l'espace colorimétrique RVB, de la mettre à l'échelle pour qu'elle corresponde à la résolution du buffer de sortie, et de dessiner le résultat sur ce buffer. Mais il s'agit d'un traitement hautement parallèle. Vous devez faire ça pour chaque pixel. Si vous avez des connaissances en infographie, vous voyez où je veux en venir : c'est un travail pour votre carte graphique !

Et pour parler à la carte graphique, on a besoin d'OpenGL.
OpenGL (Open Graphics Library) est une bibliothèque multiplateforme et un standard qui permet au programmeur de contrôler un GPU (Graphics Processing Unit) pour des tâches telles que le rendu 2D et 3D.
Elle est bien connue dans l'industrie du jeu vidéo, tout comme DirectX et Metal (des bibliothèques équivalentes qui ne sont pas multiplateformes), et elle est maintenant lentement remplacée par la nouvelle norme Vulkan.

Voici donc ce qu'il en est : Linphone utilise OpenGL dans son moteur de rendu vidéo pour effectuer la conversion Y'CbCr -> RGB.

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 version 4.1. Il y avait du code existant, que je devais soit faire évoluer, soit réécrire, ce qui dans tous les cas supposait que j'avais une assez bonne compréhension de ce qu'il faisait...

J'ai déterminé que le code existant faisait plus ou moins la conversion Y'CbCr vers RVB de cette façon :

Il s'agit d'une matrice 3x3 appliquée à chaque pixel Y'CbCr pour obtenir un pixel RVB. Une image de résolution 720P contient 921 600 pixels, ce qui représente au final beaucoup de calculs... et c'est là qu'OpenGL intervient.

OpenGL a introduit dans la version 2.0 (2004) le concept de shaders à travers le langage GLSL (OpenGL Shading Language) - un langage ressemblant au C pour exprimer les calculs à effectuer par le GPU. Initialement créés pour calculer les niveaux de lumière et de couleur lors du rendu d'un objet 3D, les shaders ont évolué pour exécuter une variété de fonctions spécialisées. Ici, les shaders sont utilisés de manière détournée mais intelligente : Il n'y a pas vraiment de scène 3D : nous dessinons simplement une texture plate à l'écran. La texture, au lieu d'être donnée sous forme de buffer RVB comme elle le devrait, est une image Y'CbCr et le shader est exécuté par la carte graphique pour transformer cette texture Y'CbCr en une texture RVB. Simple, n'est-ce pas ?

Mais d'où vient cette matrice de transformation ?

J'ai donc cherché "yuv2rgb" sur le web, ainsi que certains des nombres magiques utilisés comme "1.16438355".
J'ai pu trouver de nombreux exemples de code (principalement en C) qui utilisaient des constantes similaires pour "convertir du YUV en RGB" (peu importe ce que cela signifie), mais aucun n'expliquait comment elles étaient obtenues.
Je suis également tombé sur l'article YUV de Wikipedia, où j'ai appris que l'YUV peut en fait être formaté selon différents sous-échantillonnages chromatiques, par exemple : YUV 4:4:4, YUV 4:2:0, YUV 4:1:1, etc.
Cela n'a fait que susciter d'autres questions telles que "quel sous-échantillonnage ? ce shader est-il censé être pris en compte ?"
L'article contenait quelques chiffres et formules, mais à première vue, aucun ne semblait correspondre aux chiffres magiques de mon shader...

J'ai donc décidé de respirer, de me calmer et de prendre le temps d'étudier et de reprendre ma meilleure piste : L'article YUV de Wikipédia.
Cela m'a conduit à l'article Y'CbCr, où tout a commencé à s'éclaircir.

J'avais enfin trouvé l'origine de mes chiffres magiques : ils sont le résultat de la simplification (au sens mathématique) de la matrice de conversion inverse de la norme ITU-R BT.601, mixés à une translation et une mise à l'échelle dues à la plage de valeurs partielles !
Notamment, le facteur "1,16438355" que j'ai mentionné précédemment est une version arrondie du facteur d'échelle 255 / 219 que l'on peut trouver dans la section ITU-R BT.601 conversion de cet article.

Je sais, je sais, je ne comprends pas la moitié de tout cela non plus, mais le fait est que mes chiffres sont passés de magiques à scientifiques.
Je n'ai pas besoin de comprendre la physique de l'émission de lumière du phosphore ni comment Kr, Kg et Kb ont été choisis. J'ai juste besoin de savoir que c'est ainsi qu'ils ont été choisis.

Exemple de code avec documentation :

Après toutes mes recherches et mes efforts, voici le code final que nous utilisons aujourd'hui dans les contextes OpenGL 4.1 :

https://gitlab.linphone.org/BC/public/mediastreamer2/-/blob/master/src/Y...

J'ai essayé de concevoir ce code de manière à ce qu'il soit facile à suivre avec l'article de Wikipédia ouvert à côté, et qu'il utilise des noms explicites.

Conclusion :

Décharger des tâches de traitement spécifiques sur le GPU est souvent une bonne idée pour économiser du temps CPU et de l'énergie. Cette conversion de l'espace colorimétrique, de manière non optimisée (code C simple sans instructions SIMD), consommerait à peu près autant que l'ensemble du processus d'encodage vidéo exécuté sur le processeur principal. Vous comprendrez alors l'importance de cette optimisation dans la pipeline de traitement des flux vidéo !

La nouvelle API Vulkan, une norme ouverte pour les graphismes 3D, annonce des API de codecs vidéos accélérées par le GPU (H.264, H.265, AV1). Cela semble extrêmement prometteur pour optimiser davantage la pipeline de traitement vidéo de Linphone en déchargeant les tâches sur les GPU.

Pour tout complément d’informations, n'hésitez pas à solliciter notre équipe !