• Aucun résultat trouvé

Pour finir, FFmpeg doit gérer les multiples entrées/sorties que peut utiliser un flux multimédia. En effet, même si un flux multimédia peut être supposé écrit dans un fichier, il arrive souvent qu’un flux soit envoyé sur un réseau (RTP, TCP, UDP) ou que l’utilisateur veuille rajouter une couche de chiffrement. Pour gérer cela, FFmpeg définit un enregistrement AVIOContext contenant les données et méthodes propres à chaque type d’entrée/sortie. La figure 99 présente les différents champs de cet enregistrement. Celui-ci contient des champs servant à décrire l’entrée/sortie (Possibilité de se positionner dans le flux, la taille max du fichier, la taille maximum d’un paquet, etc) ainsi que trois méthodes permettant de faire des opérations. La première opération ,read_packet, est la lecture de données permettant de lire size octets du flux dans un buffer. La deuxième opération, write_packet, analogue, permet d’écrire size octets dans un flux. Et pour finir, l’opération seek permettant de se déplacer dans le flux si le type d’entrée/sortie en question le permet. Les noms utilisés pour ces opérations ne sont pas forcément adaptés car ils laissent transparaître qu’elles écrivent où lisent des paquets

alors que ces méthodes travaillent sur des tampons de mémoires bruts. libavformat::AVIOContext maxsize: int max_packet_size: int seekable: bool read_packet(buffer, size) write_packet(buffer, size) seek(offset)

Figure 99. Diagramme de classe Uml de l’enregistrement AVIOContext de FFmpeg.

Maintenant que nous avons décrit les trois types de données manipulés par FFmpeg, nous allons détailler comment la bibliothèque passe d’un type à l’autre en utilisant des codecs et des formats.

2 Les codecs

Les codecs sont déclarés en instanciant des objets de type AVCodec dont le patron est visible dans la figure 100. Cet objet contient les informations spécifiques de ce codec, en particulier, son nom, le type de données que le codec peut traiter, la norme qu’implémente ce codec (spécifiée par un identifiant unique dans FFmpeg), ainsi que les fonctionnalités/caractéristiques de l’implémentation. Ces fonctionnalités peuvent être diverses et variées comme pouvoir permettre l’encodage sans perte d’une donnée, l’encodage de manière parallèle, l’encodage en deux passes, l’encodage assisté par du matériel, etc. En addition à ces champs, l’objet contient cinq méthodes permettant d’utiliser ce codec. Toutes ces méthodes ont en paramètre le contexte servant à garder l’évolution d’une instance de compression ou de décompression.

- La méthode init est exécutée une seule fois au début de la compression ou de la décompression et permet d’initialiser le contexte du codec.

- La méthode encode2 est utilisée pour encoder des données brutes en données compressées. Cette méthode ne retourne pas toujours de paquet. Comme dit précédemment, les compresseurs tels que h264 ou vp8 ne compriment pas forcément les images directement. Ils stockent une séquence d’images de manière à réorganiser le flux de manière optimale. De ce fait lors de la phase de mise en tampon, lorsque la fonction encode2 est exécutée, le compresseur va mettre en tampon l’image courante en attendant les prochaines exécutions de la fonction encode2pour recevoir davantage d’images. De la même manière, pour encoder un

libavcodec::AVCodec name: string type: AVMediaType id: AVCodecID capabilites: flags init(ctx)

encode2(ctx, pkt, frame, got_packet) decode(ctx, out_data, out_size, pkt) close(ctx)

flush(ctx)

Figure 100. Diagramme de classe Uml de l’enregistrement AVCodec de FFmpeg.

flux audio, certains codecs attendent d’avoir un minimum d’échantillons afin de pouvoir créer un paquet entier.

- La méthode decode est utilisée pour faire l’opération inverse c’est-à-dire transformer un paquet en données brutes. Pour les mêmes raisons que la méthode encode2, la méthode decode peut ne pas retourner de trame. Dans ce cas la taille des données retournées sera zéro.

- La méthode close est exécutée une dernière fois à la fin d’un contexte de compres-sion ou de décomprescompres-sion. Elle permet de récupérer les ressources allouées lors de l’initialisation.

- La méthode flush permet à l’utilisateur de vider toutes les données mises en attente par le codec. Cette fonction est utilisée principalement lorsque le flux de sortie est réseau et qu’il a besoin de garantir des propriétés de temps réel.

Cet objet, unique à chaque implémentation de codec, est déclaré comme étant une variable globale à la bibliothèque. Cette variable globale est ensuite utilisée lors de la phase d’initialisation de FFmpeg, dans la fonction avcodec_register_all, afin d’enregistrer le codec dans sa base de codecs. De ce fait, quand FFmpeg a besoin d’un codeur ou d’un décodeur, il va chercher l’implémentation du codec dans sa base de codecs. Une fois le codec trouvé, il initialise un contexte pour ce codec et initialise ensuite le codec en lui-même grâce à la méthode init. Il peut ensuite demander au codec de compresser une donnée avec la fonction encode2 ou de décompresser une donnée avec la fonction decode.

3 Les formats

À la différence des codecs, la gestion des formats est scindée en deux, les formats d’entrée et les formats de sortie. Un format d’entrée aussi appelé demuxeur permet de lire un flux d’entrée et de séparer les différents médias afin de fournir des paquets. L’enregistrement servant à contenir les informations d’un format d’entrée est AVInput-Format. De façon analogue, un format de sortie appelé muxeur entrelace et écrit des

paquets dans un flux de sortie et est contenu dans l’enregistrement AVOutputFormat. Tout comme pour les codecs, les formats nécessitent un contexte permettant de stocker les données utiles à la réception ou à l’émission d’un flux, ce contexte est passé en paramètres aux méthodes de l’enregistrement. Nous détaillerons dans un premier temps la gestion des formats d’entrée puis la gestion des formats de sortie.

Les formats d’entrées sont déclarés via une variable de type AVInputFormat. Ce type est représenté par la figure 101. Cet enregistrement ne contient que deux membres, le nom du format et des flags indiquant les capacités de l’implémentation comme le support de la lecture sur réseau, la possibilité de se déplacer dans le flux (seek).

libavformat::AVInputFormat name: string flags: flags read_probe(data) read_header(ctx) read_packet(ctx, pkt) read_close(ctx) read_seek(ctx, timestamp) read_timestamp(ctx) read_play(ctx) read_pause(ctx)

Figure 101. Diagramme de classe Uml de l’enregistrement AVInputFormat de FFmpeg.

Les méthodes associées à l’enregistrement sont :

- la méthode read_probe est un peu particulière car elle ne prend pas en paramètre le contexte mais un buffer de données contenant le début d’un flux. La méthode doit déterminer si les données correspondent au format implémenté. Cette méthode est utilisée pour déterminer le type de flux quand celui-ci n’est pas connu. - La méthode read_header permet de lire l’entêtes du flux d’entrée ainsi que

l’ini-tialisation du contexte de lecture. Cette méthode est très importante car elle permet de connaître toutes les informations contenues dans un flux et déclenche l’initialisation des codecs utiles à la décompression du flux.

- La méthode read_packet est la méthode principale pour récupérer des paquets d’un flux. Cette méthode renvoie séquentiellement les paquets arrivant d’un flux. Ces paquets peuvent ensuite être décompressés en fonction de leur codec. Les paquets sont retournés dans l’ordre de compression.

- La méthode read_close ferme un flux d’entrée et libère les ressources allouées lors de la lecture de l’entête.

- La méthode read_seek déplace le curseur interne de l’implémentation vers un nouveau paquet, permettant ainsi de se positionner dans le flux à une position bien précise. La position vers laquelle le flux se positionne est définie par le

timestamp passé en paramètre. Cette méthode peut être utilisée que sur des flux donc tout le contenu est disponible comme un fichier.

- La méthode read_timestamp renvoie le prochain timestamp dans le flux.

- La méthode read_play lance la lecture d’un flux. Cette méthode est utilisée lorsque le flux est un flux réseau.

- La méthode read_pause met en pause la lecture d’un flux. Cette méthode est utilisée lorsque le flux est un flux réseau.

De manière similaire, les formats de sorties sont eux déclarés par une variable de type AVOutputFormat. L’enregistrement AVOutputFormat est illustré par la figure 102. Il contient les mêmes membres que AVInputFormat contenant le nom et les capacités du format, tout en rajoutant trois nouveaux membres contenant les identifiants des codecs utilisés par défaut pour le format.

libavformat::AVOutputFormat name: string flags: flags video_codec: AVCodecID audio_codec: AVCodecID sutitle_codec: AVCodecID write_header(ctx) write_packet(ctx, pkt) write_trailer(ctx) query_codec(CodecID)

Figure 102. Diagramme de classe Uml de l’enregistrement AVOutputFormat de FFmpeg.

Les méthodes pour cet enregistrement sont très similaires aux méthodes utilisées pour la lecture de flux. Elles sont :

- la méthode write_header initialise le contexte du flux de sortie et écrit les entêtes du format dans le flux.

- La méthode write_packet écrit un paquet dans le flux de sortie. Le paquet en question peut être mis en tampon le temps d’avoir plus de données à écrire. - La méthode write_trailer permet d’écrire la fin de fichier ainsi que de libérer les

ressources allouées pour l’écriture du flux.

- La méthode query_codec permet de savoir si un codec est supporté par le format en question.

Chaque format a donc deux variables globales afin de gérer l’écriture et la lecture d’un flux, une variable AVInputFormat et une variable AVOutputFormat. Les différents muxeurs et demuxeurs sont enregistrés dans le code de FFmpeg dans la fonction av_register_all. Un format peut n’avoir que le muxeur ou que le demuxeur implémenté en fonction du support pour le format en question.

Les codages entropiques

1 Le codage RLE

Le codage RLE1 est un codage plus simple que le codage de Huffman. Il a été développé de manière à encoder des sources de symbole suivant un processus de Markov stable2. Il est notamment très bon pour les textes numérisés en noir et blanc (deux valeurs) car la probabilité qu’un pixel voisin soit d’une couleur différente est très faible.

Le principe est d’encoder les successions de symboles plutôt que les symboles seuls. L’encodeur compte donc le nombre de fois qu’un symbole apparaît, et écrit ce nombre ainsi que le symbole en lui-même. Par exemple, le message AAACCDEEEEE est encodé en 3A2C1D5E. L’avantage de ce codage est sa simplicité de mise en œuvre. Cependant, dans le cas où le message est instable, l’encodage sera plus long que le message initial. Par exemple, le message ACDBEADBBE sera encodé en 1A1C1D1B1E1A1D2B1E résultant en une perte de compression. Ce schéma pathologique est très souvent rencontré dans la compression d’image car les valeurs de pixel voisin, même si proche, sont toujours différentes ceci dû aux textures des objets de la scène ou au bruit d’acquisition.

2 Le codage de Huffman

Le codage de Huffman [Huffman, 1952] est probablement le codeur entropique le plus connu grâce à sa simplicité et son efficacité. Il est utilisé dans des compresseurs connus comme JPEG ou le MP3. Il produit, à partir de statistique préalablement définie, des codes à longueur variable pour représenter les symboles de la source de données. De ce fait, une première étape de génération de la correspondance entre chaque symbole de la source et son encodage est faite. Une fois cette correspondance établie (celle-ci peut être réalisée dynamiquement ou statiquement), l’encodeur se retrouve simplement à lire chaque symbole et d’écrire le code correspondant comme sortie. Pour trouver le

1. RLE : Run Length Encoding

2. Nous définirons un processus de Markov stable comme un processus dont la probabilité de changer d’état est bien plus faible que de rester dans le même état. Il est dit stable car il ne change pas souvent d’état.

code correspondant, il est possible d’utiliser une table de correspondance simple ou un tableau associatif en fonction de la forme des symboles d’entrée. La particularité de ce codage est dans l’établissement de cette correspondance. Un arbre de codage (aussi appelé arbre de Huffman) est utilisé pour établir la correspondance entre les différents symboles et leur code. Cet arbre est un arbre binaire et il est construit en suivant l’algorithme 3.

Algorithme 3. Construction de l’arbre de Huffman

Entrées : L : Liste d’arbres à un nœud pour chaque symbole contenant le symbole

ainsi que sa probabilité d’apparition

Sorties : A : Arbre de Huffman

1 Function ContruireArbreHuffman

2 tant que la liste contient plus d’un nœud faire

3 Rechercher les deux arbres a1et a2dont les probabilités sont les plus faibles; 4 Retirer a1 et a2 de la liste L;

5 Créer un nœud N ayant comme probabilité la somme des probabilités des arbres a1 et a2;

6 Définir comme fils de N les arbres a1 et a2; 7 Insérer ce nouvel arbre L;

8 fin

9 retourner L’arbre restant de la liste

De manière à illustrer l’algorithme de construction de cet arbre, nous proposons un exemple en utilisant l’alphabet suivant A, B, C, D, E avec les probabilités correspondantes 0.1, 0.2, 0.15, 0.5, 0.05. La sous-figure 103a représente la liste d’arbres qui sont initialement composés de seulement un nœud. Les nœuds entourés d’un rectangle pointillé sont les nœuds racines des arbres présents dans la liste d’arbres. Les deux nœuds colorés A et E qui ont les probabilités les plus faibles sont utilisés pour faire un nouvel arbre. Cet arbre est constitué d’un nouveau nœud donc la probabilité est la somme des probabilités de ces fils et des deux nœuds comme fils de ce nouveau nœud. Le résultat de cette opération est visible dans la sous-figure 103b. Les deux nouveaux nœuds sélectionnés sont maintenant la racine de l’arbre qui vient d’être créé et le nœud C. Un nouvel arbre est créé avec une probabilité de 0.3 et comme fils le nœud C et l’ancien arbre. La sous-figure 103c synthétise cette étape. L’algorithme continue ainsi jusqu’à n’avoir qu’un nœud dans la liste d’arbres. Ce nœud est la racine de l’arbre de Huffman. Le résultat de l’algorithme est donc l’arbre visible dans la sous-figure 103e. De manière à déduire les codes pour chaque symbole, il faut parcourir en profondeur cet arbre de la racine vers la feuille du symbole en question. Quand nous effectuons une descente vers la branche gauche, nous associerons la valeur binaire 0 et pour une descente vers la branche droite une valeur binaire 1. Toutes les valeurs binaires concaténées constituent le code. De ce fait, pour avoir le code du symbole C, nous effectuons deux descentes à droite

(a) 0.1 A 0.2 B 0.15 C 0.5 D 0.05 E (b) 0.2 0.15 0.5 0.15 0.05 0.1 C D B A E (c) 0.2 0.5 0.3 0.15 0.15 0.05 0.1 C D B A E (d) 0.5 0.5 0.2 0.3 0.15 0.15 0.05 0.1 C D B A E (e) 1 0.5 0 0.5 0.2 0 0.3 0.15 0 0.15 0.05 0 0.1 1 1 1 1 C D B A E (f) Symbole Code A 1110 B 10 C 110 D 0 E 1111

Figure 103. Exemple d’élaboration d’un arbre de Huffman. Les sous-figures

103a,103b,103c et 103d montre les étapes de construction de l’arbre de Huffman. La sous-figure 103e montre l’arbre final et la sous-figure 103f expose la correspondance entre les symboles et les codes d’après le codage de Huffman.