Archives pour août 2009

h1

Avancement pour les “grabbers”

2009.08.23

Hello,

Heidrunplusieurs grabbers ont intégrés Valhalla depuis le dernier article, merci à Benjamin pour son travail. Il reste cependant quelques lacunes à combler. Certaines des fonctionnalités manquantes sont mineurs et je ne vais donc pas les aborder ici. Même que je ne compte pas travailler dessus tant que les plus importantes ne seront pas implémentées.

Scan “on-demand”

C’est sûrement une des fonctionnalités la plus importante à faire avant l’intégration dans Enna. Son rôle est de permettre la récupération des méta-données en priorité (sur demande). L’idée est très simple, lorsque vous utilisez Enna et ciblez un fichier en particulier, le rôle du “on-demand” sera de fournir toutes les données le plus rapidement possible pour ce fichier. A moins que ces données soient déjà récupérées et sauvées dans la base de données (ainsi que les fichiers téléchargés tel que les couvertures).

Bien que l’idée soit simple, l’implémentation est bien plus complexe. Valhalla étant un scanner de répertoire, il n’y a actuellement aucun mécanisme pour donner une priorité plus importante à un fichier plutôt qu’un autre. Comme je l’explique dans l’article précédent, les tâches des threads de Valhalla fonctionnent selon le principe du premier arrivé, premier servi. Il faut donc implémenter un moyen de mettre “un paquet” selon un LIFO (Last In, First Out) à la place d’un FIFO. Par exemple un attribut indiquerait que le paquet est prioritaire et les queues seraient utilisées en LIFO.
Les queues dans Valhalla sont héritées de libplayer. Elles se nomment fifo_queue et n’ont pas du tout été prévues pour le LIFO comme le suggère leur nom. Il est peut être temps de les rendre plus polyvalentes. Je n’ai encore rien entrepris en ce sens, je ne fais que des réflexions sur une manière parmi d’autres de gérer le “on-demand”.

Donner plus de priorité dans les queues ce n’est pas l’unique chose à prendre en compte. Le “on-demand” doit aussi pouvoir se faire sur des fichiers qui ne sont pas disponibles avec les chemins scannés par Valhalla. Ce qui induit que ces entrées doivent avoir un statu particulier dans la base de donnée, afin que Valhalla ne les supprime pas lors du prochain scan, croyant que se sont des fichiers effacés car introuvables. Quoi qu’il en soit, avant d’écrire la moindre ligne de code, il est important de faire toutes les considérations nécessaires afin de ne pas introduire des modifications trop intrusives aussi bien au niveau interne de Valhalla qu’au niveau de la structure de la base de donnée.

Le “force-stop”

Depuis la première version de Valhalla, il est possible d’arrêter un scanner (et donc tous les threads en aval) même si le système est en train de travailler sur des fichiers à différents niveaux. Le fait d’avoir ajouté les grabbers à fortement compliqué cette fonctionnalité de “force-stop” qui n’est actuellement pas optimale. J’ai du faire un choix afin de pouvoir commiter le code sur les grabbers même si je n’ai pas terminé cette part du travail.

Mais tout d’abord, pourquoi le “force-stop”? Lorsque vous êtes dans Enna, Valhalla tourne en arrière plan avec une priorité très basse afin de ne pas influencer l’interface et les lectures des fichiers. Si vous quittez Enna, vous voulez le faire immédiatement ce qui semble logique. Par contre pour Valhalla c’est plus compliqué. L’arrêter en plein au milieu de tâches l’oblige à ne pas pouvoir vider les queues des threads. Toutes les données dans ces queues sont donc libérées et perdues. Avant l’ajout des grabbers ce n’était pas du tout un problème, car le système se limitait à une seule étape; le parser. Un thread n’est pas tué avec le “force-stop”, mais sa dernière tâche est terminée normalement. La différence est que les tâches suivantes sont ignorées et le thread se termine comme s’il n’y avait rien de plus à faire. Ainsi avant l’ajout des grabbers, au pire les fichiers restant dans les queues n’étaient pas insérés/actualisés dans la base de donnée. Les fichiers qui étaient déjà insérés l’étaient au complet. Ceux qui ne l’étaient pas, ne l’étaient pas du tout. C’était du tout ou rien. Le prochain scan (démarrage d’Enna) permettait de continuer le travail très simplement. Les queues se repeuplaient par le scanner qui ne trouvait pas les fichiers dans la base de données, et ignorait ceux qui existaient déjà (c’est toujours le cas “plus-ou-moins” si vous compilez Valhalla sans les grabbers).

Depuis que les grabbers sont présents, la complexité du “force-stop” a augmenté pour deux raisons. Un fichier peut exister dans plusieurs queues en même temps, et un fichier doit passer par plusieurs étapes avant d’être terminé. Les données d’un fichier dans la base de donnée ne peuvent plus être considérées comme du tout ou rien. Un fichier se retrouve complètement fragmenté quand les threads sont tous terminés en plein au milieu des tâches.

Fonctionnement actuel

J’ai donc fais un choix intermédiaire pour que Valhalla fonctionne aussi en “force-stop” bien que le problème de fragmentation ne soit pas réglé. J’ai décidé de traiter un fichier comme c’est le cas sans le support des grabbers mais à une différence près. Un fichier est considéré comme interrompu tant qu’il n’a pas atteint l’étape ENDING (voir l’article précédent). Donc au prochain scan (redémarrage de Valhalla), ce fichier repassera absolument par toutes les étapes. Le problème est donc que les données déjà grabbées (et sauvées dans la DB) sont re-grabbées ce qui prend beaucoup de temps inutile. Avec des milliers de fichiers et beaucoup de grabbers, il se trouve que Valhalla passe son temps à refaire encore et encore le même travail si on ne le laisse pas se terminer de lui même. Car pour qu’un fichier puisse atteindre l’étape ENDING quand il y a 5-6 grabbers, il faut se lever tôt ;-).
Ce système est provisoire! Il est là en attendant que j’y travail. Et c’est une des raisons qui fait que Valhalla ne doit pas encore être utilisé avec les grabbers dans Enna.

Pourquoi de la fragmentation? Alors ça n’a rien à voir avec votre disque dur, quand je parle de fragmentation c’est dans le sens où toutes les informations qui devraient l’être ne sont pas disponibles dans la base de donnée. Par exemple un fichier est passé par l’étape PARSING et GRABBING (le premier grabber). Les méta-données du parser ont été sauvées dans la DB, ainsi que celle du premier grabber par exemple. Néanmoins il y a peut être encore 4 grabbers avant le downloader et l’ENDING. Le “force-stop” arrête tout, vide les queues, quitte Valhalla. Il se trouve que le fichier en question a une partie des méta-données dans la base de donnée, mais on a aucune certitude qu’elle soient toutes disponibles car ce fichier est indiqué comme étant interrompu.

Fonctionnement futur

Je suis en train de réfléchir à ce problème (sur papier pour le moment). Ce qu’il faut arriver à faire, c’est de sauver le contexte des fichiers interrompus pour pouvoir restaurer leur contexte à la prochaine exécution de Valhalla. Il n’y a donc pas 50 manières de faire. La sauvegarde du contexte doit être réalisée dans la base de donnée. Un mécanisme à l’exécution de Valhalla devra chercher les contextes, les restaurer puis les effacer de la base de données.

Un contexte devra contenir les informations sur quels grabbers ont été traités en entier (ceux où les données sont vraiment sauvées) ainsi que la liste des fichiers qui doivent être téléchargés. Il faut aussi considérer les cas où des contextes ont été sauvés mais que certains grabbers ne sont plus disponibles avec Valhalla, ou même qu’un Valhalla sans le support des grabbers essaient d’utiliser la base de données qui contient ces contextes.

Concernant le downloader c’est un peu différent par rapport aux autres threads. Comme je l’ai dis précédemment, un thread termine sa tâche en cours puis quitte. Pour le downloader une tâche peut être interrompue en plein au mieux. Cela veut simplement dire que pour un fichier en particulier, il y a par exemple un “cover”, un “backdrop” et peut être encore autre chose à télécharger. Il se peut que le “cover” soit téléchargé, que le “force-stop” se manifeste et donc le “backdrop” est ignoré. La tâche est à moitié réalisée. Il est donc nécessaire de sauvegarder l’état de cette tâche avant de quitter le thread du downloader.

Finalement ça représente des ajouts de tables dans la base de données pour sauvegarder les contextes, des mécanismes pour gérer les contextes (sauvegarde, restauration et effacement), puis ensuite on pourra réfléchir sérieusement à l’intégration dans Enna.

Les évènements

Le dernier point relativement important est la gestion des évènements pour un scan “on-demand”. Les données étant insérées au fur et à mesure dans la base de donnée, il est intéressant de recevoir une information sur ce qui est disponible afin de savoir quand aller les lire. Simplement, lorsque vous êtes sur un fichier en particulier avec Enna, un “backdrop” va se télécharger à un moment indéterminé. Un évènement sera envoyé par Valhalla à Enna quand le fichier sera sauvé et le “backdrop” s’affichera.

C’est avant tout une question de confort que d’avoir cette fonctionnalité. Dans le cas contraire il serait nécessaire d’aller lire les données que lors de l’accès au fichier, et les images par exemple ne s’afficheraient qu’en changeant de fichier, puis en revenant sur le précédent (pour autant que quelque chose s’est téléchargé entre temps).

Il y a d’autres points mais moins prioritaires. Je compte travailler spécialement sur les trois problèmes présentés dans cet article. J’ai déjà fais un peu de travail sur le “force-stop”. Après je pense régler le “on-demand” puis les évènements.

A bientôt,

Mathieu SCHROETER

h1

Le support des “grabbers” dans la Valhalla

2009.08.11

Les feuilles d’Yggdrasil ne sont plus si inaccessibles pour les braves de la Valhalla. Les “grabbers” auraient pu s’appeler Heidrun mais on se contentera d’un nom plus technique.

Les “grabbers” sont donc enfin supportés dans libvalhalla. Il y a encore du travail avant de pouvoir les utiliser correctement depuis Enna, mais l’essentiel est là. Pour ceux qui n’ont aucune idée de ce qui se cache derrière ce mot barbare “grabber”, on pourrait le définir simplement comme étant une manière de s’accaparer (si on traduit directement du mot anglais) ou alors de récupérer des données depuis des moyens extérieurs.
Par exemple, libvalhalla peut récupérer des méta-données provenant directement des fichiers scannés, à l’aide d’FFmpeg (le parser). Mais si on désire retrouver la couverture CD/DVD il n’était pas possible de le faire jusqu’ici. C’est là qu’intervient le “grabber” tel que celui pour Amazon. On peut imaginer plein d’autres types de “grabbers”, mais celui d’Amazon est le seul porté sous Valhalla au moment où j’écris ce billet et il permet de “rapatrier” les couvertures CD/DVD.

Architecture

L’architecture de la bibliothèque à été profondément changée. Trois nouveaux threads ont fait leur apparition et sont visibles dans l’image ci-dessous en tant que “Dispatcher”, “Grabber” et “Downloader”.

Valhalla Internals

L’image correspond au code, néanmoins elle est un petit peu simplifiée afin d’être suffisamment lisible et compréhensible. Le Dispatcher peut être vu comme un switch réseau. Son but et d’être très réactif (il ne fait donc pas grand chose) et de transmettre les “paquets” au bon endroit en fonction de l’état de celui-ci. Un “paquet” est une analogie au réseau, mais finalement dans le cas de libvalhalla, ce n’est rien de plus qu’une structure qui défini un fichier.

Afin de bien comprendre l’architecture, je vais détailler le chemin effectué par un paquet. Lorsque le scanner trouve un fichier sur le disque, il “l’empacte” et le transmet au DBManager. Le DBManager va interroger la base de donnée pour savoir si ce fichier existe déjà, si oui il vérifie la date de dernière modification et ignore ce paquet si cette date n’a pas changée, autrement (ou si le fichier n’existe pas dans la base), il transmet le paquet au Dispatcher. Un paquet doit suivre 4 étapes avant d’être détruit.

  • Étape 0: PARSING
    Récupération des méta-données à l’aide d’FFmpeg
  • Étape 1: GRABBING
    Récupération de nouvelles données et (ou) fichiers texts (XML) à l’aide d’un ou de plusieurs grabbers
  • Étape 2: DOWNLOADING
    Téléchargement des fichiers indiqués par le(s) grabber(s) (couvertures CD par exemple)
  • Étape 3: ENDING
    Finalisation et destruction du paquet

Le processus pour un paquet se résume donc en:

Scanner -> DBManager -> Dispatcher -> Parser -> Dispatcher -> Grabber -> Dispatcher -> Downloader -> Dispatcher -> DBManager -> Scanner.

Ce qui est faux en réalité, mais l’idée est correcte. L’intérêt de présenter le processus aussi simplement est uniquement là pour permettre de comprendre par la suite comment valhalla fonctionne. Si un paquet suivrait vraiment le processus ci-dessus, le système serait extrêmement lent (j’exagère peut être sur le mot “extrême”) mais il faut garder en tête que travailler avec des grabbers ça ne peut que ralentir, surtout s’il y a beaucoup de grabbers et s’ils font des accès sur internet.

Comme un pipeline

Si le Dispatcher, le Parser, le Grabber et le Downloader sont sur des threads différents ce n’est pas juste pour pouvoir traiter plusieurs paquets en parallèles, mais aussi pour pouvoir traiter un même paquet à plusieurs endroits en même temps. L’idée est donc de récupérer les données “parsées” et “grabbées” aussi vite que possible dans la base de donnée. Ainsi même si un paquet n’a pas finit de suivre toutes les étapes du processus, il est qu’en même possible d’aller chercher les informations dans la base de données.

Pour ceux qui connaissent un peu les architectures des processeurs, ils connaissent également la représentation en pipeline du cycle d’exécution des instructions. J’ai vais donc expliquer le système par un dessin selon ce principe.

Valhalla Pipeline

Ce pipeline présente 4 fichiers traités par deux parsers parallélisés. Il y a chaque fois deux grabbers en série pour chaque fichier, avec un downloader à la fin du processus.

Les tâches des threads de Valhalla fonctionnent en tant que FIFO (à ne pas confondre avec l’ordonnanceur du noyau). Le premier arrivé est donc le premier servi. Chaque fichier de ce pipeline peut être séparé en deux lignes. C’est ce qui arrive lorsqu’un grabber est en marche et que des méta-données doivent être sauvées dans la base de donnée. C’est cela qui permet d’avoir un temps de réaction intéressant. Il est inutile d’attendre que le processus soit terminé pour avoir les méta-données. Par exemple avec le “FILE 0″, le Scanner (jaune) transmet le paquet au DBManager (rose pâle), qui va le transmettre au Dispatcher (gris) afin d’être “parsé” (rose). Puis le paquet revient au Dispatcher et se voit transféré dans deux threads, le grabber (vert) et le DBManager. A ce moment là il se passe deux choses, le DBManager va insérer les méta-données du Parser dans la base de donnée et en même temps le grabber va commencer à traiter les nouvelles méta-données. Puis au “grabbing” suivant, c’est les méta-données du grabber précédent qui sont sauvées quand le deuxième grabber s’apprête à traiter les nouvelles méta-données. Au final c’est lors de l’étape du downloading que les dernières méta-données sont sauvées.
En résumé, chaque case DBManager (de demi-hauteur) correspond à une insertion dans la base de donnée. Dans le cadre de cet exemple, il y a donc trois insertions par fichier (le parser + les deux grabbers).

J’ai omis quelques informations sinon cet article serait 5 fois plus long.

Il faut également prendre le diagramme avec des pincettes car il est impossible de prédire la forme exacte pour plusieurs raisons. Les cases “parser”, “grabber” et “downloader” en particulier sont très disproportionnées. Leur temps est une question de plusieurs dizaines de millisecondes à plusieurs secondes. Tout dépend de la taille des fichiers, du demuxer utilisé par FFmpeg, de la vitesse de votre connexion internet, du temps sur les accès aux disque dur, etc, … Le dispatcher se contente de quelques microsecondes, et dans le diagramme il prend autant de temps que le DBManager ce qui est absurde.
Au premier abord on pourrait penser qu’il y a beaucoup de trous dans ce pipeline, mais en réalité les trous sont bien plus grand que ça si vous considérez qu’un parser prend 3 secondes pour un fichier. Néanmoins ce n’est pas du tout un problème (d’ailleurs s’il n’y avait pas de trous alors tous vos CPU seraient constamment à 100%; il ne faut pas oublier non plus que dans certaines étapes il y a des temps morts tel que les accès au disque dur et sur internet).

Si vous regardez bien le diagramme, vous voyez des trous importants après les étapes “parser”. En fait, le scan du disque, le parsing et l’insertion des méta-données des parsers vont se faire très vite. A la même vitesse qu’avant l’ajout des grabbers dans Valhalla. Les grabbers ont aucun impacte sur les parsers car ils sont exécutés après eux. Ce n’est pas plus compliqué que ça.

Il faut également interpréter ce diagramme d’un point de vue plus large. Imaginez le avec plus de 100 lignes (ou plus), ce qui peut arriver sans problème lorsqu’un scanner passe sur un de vos dossiers de musique. Cela donnerait visuellement tous les parsers qui descendraient à gauche en un escalier serré, et les grabbers seraient parsemés (avec des trous importants dans toutes les lignes). Néanmoins il peut y avoir des grabbers non utilisés par certains fichiers, voir même aucun grabber, ce qui complique fortement le diagramme.

Quelques précisions

Il y a de la documentation avec les en-têtes de libvalhalla afin de savoir comment implémenter un grabber, mais je donerais des informations sur ce blog un de ces 4.

L’application test-valhalla permet de tester les grabbers. Il suffit de lire l’aide de la commande.

Valhalla peut être compilé sans le support des grabbers, ainsi les fichiers passent de l’étape 0 PARSING à l’étape 3 ENDING. La bibliothèque réagira exactement comme avant l’ajout des grabbers.

Il manque également  des éléments pour une utilisation dans Enna, le fichier TODO vous en dira plus.

Voilà, je n’ai pas la motivation d’en dire plus aujourd’hui, rien que de dessiner le pipeline ça m’a pris pas mal de temps. Je reviendrais donc sur certains aspects dans un prochain billet.

A bientôt,

Mathieu SCHROETER

h1

La liste de lecture de libplayer

2009.08.02

Hello,

cet été j’ai bossé un peu sur libvalhalla pour ajouter le support des grabbers. Le travail n’est pas terminé et rien n’a encore été commité car les modifications sont très intrusives et forment un tout. Je n’ai encore rien à présenter de concret mais j’espère en terminer avec ça les deux premières semaines d’août. Soit mes deux dernière semaines de congé.

La liste de lecture

libplayerMais ce n’est pas de Valhalla que je désire parler. Mais plutôt de libplayer et de quelques fonctionnalités qui existent depuis bien longtemps et qui ne sont pas forcément connues et qui ne seront même jamais utilisées par Enna. Avant l’entrée en jeu d’Enna dans le monde de GeeXboX il y a eu quelques tentatives de nouveaux GUI tel que MPUI, OMCv1, OMC-SDL puis OMCv2. Aucun de ces projets n’a abouti mais libplayer a été créé en parallèle à OMCv1 (je précise v1 et v2 car il y a eu deux tentatives différentes de créer un GUI basé sur les EFL). Pendant une période, il avait été proposé de laisser la gestion des listes de lecture à libplayer. Au début cette gestion était très rudimentaire et ne permettait que d’ajouter des fichiers dans la liste. J’ai donc pris l’initiative d’étendre la gestion de ces listes, et spécialement avec l’ajout du superviseur (cf. libplayer-et-le-multi-threading) qui date d’une année.

Libplayer contient une playlist interne. Elle n’est pas accessible depuis l’API publique et uniquement une playlist peut exister dans un player à la fois. Cette liste se présente en une structure chaînée dans laquelle il est possible de progresser en avant ou en arrière. Cela implique que l’ordre d’insertion des fichiers défini l’ordre de lecture. Ainsi il existe deux manières de lire la liste. Pour cela il m’est nécessaire de présenter quelques fonctions de l’API publique, ainsi qu’une fonction interne un peu différente.

Chaque entrée dans la playlist est un morceau de la structure chaînée, visible en tant que type opaque nommé mrl_t. Il y a quelques fonctions de base pour travailler avec ces type, tel que mrl_new(), mrl_free() et différents accesseurs. Mais il y a également des fonctions pour assigner un mrl_t à la playlist. Tel que player_mrl_set() et player_mrl_append() qui permettent d’insérer un mrl à l’emplacement courant, respectivement à la suite du mrl courant. Ainsi une série de player_mrl_append() vont permettre de remplir la liste de lecture. L’ordre d’insertion étant directement lié à l’ordre de lecture. Il existe également des fonctions pour supprimer des mrls tel que player_mrl_remove() et player_mrl_remove_all().
Tout ceci n’est qu’une présentation rapide de quelques fonctions de base. Certaines d’entre elles sont utilisées par Enna car il faut bien au moins assigner un mrl_t à libplayer pour qu’un fichier (url, dvd, …) puisse être lu. Mais il existe d’autres fonctionnalités pour effectuer des lectures automatisées de la liste de lecture. Ces fonctions n’ont aucun intérêt pour Enna qui gère déjà ses propres listes de lecture.

Les fonctions de playback

Il y a trois fonctions de playback intéressantes pour la liste de lecture de libplayer.

  • player_set_playback()
    Deux modes peuvent être défini, soit PLAYER_PB_SINGLE ou PLAYER_PB_AUTO. Dans le cas de “single”, seul le mrl en cours sera lu lors d’un appel à player_playback_start(). Le mode “auto” va permettre à libplayer de lire toute la liste de lecture sans une intervention de l’extérieur. A chaque morceau qui se termine, libplayer va passer au suivant jusqu’à atteindre la fin de la liste.

Le fait de lire la liste depuis la position courante jusqu’à la fin n’est pas toujours très intéressant, il existe donc deux fonctions qui sont très souvent présentes dans ce type de logiciel.

  • player_set_loop()
    Pour autant que le mode est en “auto”, il est possible de dire à libplayer de répéter le même morceau un certain nombre de fois avec PLAYER_LOOP_ELEMENT, ou alors de répéter la liste de lecture avec PLAYER_LOOP_PLAYLIST. Un paramètre de cette fonction permet de dire combien de fois et si ce paramètre est négatif, le bouclage est infini.
  • player_set_shuffle()
    Ici aussi, le mode de playback doit être “auto”. Ainsi la liste de lecture sera mélangée et le morceau lu suivant sera différent de l’ordre d’insertion des morceaux dans la liste de lecture. Chaque activation et désactivation du “shuffle” va réinitialiser la liste de lecture et donc mélanger l’ordre.

Bien sûr il est possible de combiner les fonctionnalités. Si le “loop” est indiqué sur PLAYER_LOOP_PLAYLIST et que le “shuffle” est activé, chaque fois que libplayer arrive à la fin de la liste de lecture, il la mélangera à nouveau et recommencera autant de fois qu’il a été spécifié avec player_set_loop().
Vous vous dites qu’il n’y a rien d’exceptionnel la dedans, vous avez tout à fait raison. Mon but est uniquement de présenter quelques fonctionnalités exploitables partiellement que par un seul outil “test-player” et forcément ignorées d’Enna. Lorsque test-player est lancé, il suffit d’utiliser [#] pour changer de mode, [.] pour changer le bouclage (il y a quelques valeurs prédéfinies) et [,] pour mélanger la liste.

L’automatisation de la lecture

J’ai parlé au début d’une fonction un peu différente. Cette fonction permet de rendre possible la lecture bouclée et mélangée. Mais avant ça un petit peu de théorie sur libplayer. Bien qu’il n’y a à priori rien d’exceptionnel d’automatiser la lecture dans libplayer, il faut qu’en même se poser une question. Le fait d’automatiser la lecture présage l’existence d’une boucle. Ainsi lorsqu’un fichier est lu en entier (donc un événement du type EOF est généré) il faut forcer le passage au morceau suivant d’une manière ou d’une autre.

Imaginons que le playback a été défini en “auto” et que la lecture a été amorcée via player_playback_start(). Cette fonction de start ne doit en aucun cas être bloquée jusqu’à la fin de la lecture. La chaîne d’exécution peut se visualiser ainsi:

démarrage de la lecture -> sélection du morceau -> lecture -> événement de fin de lecture -> sélection du morceau suivant -> lecture -> etc, …

Il y a une rétroaction et ça complique grandement les choses. Le premier démarrage de la lecture est amorcé à l’extérieur via l’utilisateur et cette fonction n’est pas bloquée jusqu’à l’EOF (en réalité elle bloque jusqu’à ce que la lecture soit réellement lancée). Donc le programme qui utilise libplayer peut faire autre chose en même tant que le média se lit. Il est donc nécessaire d’intercepter la fin de la lecture à l’intérieur de libplayer pour que se sois la bibliothèque qui passe au morceau suivant. Le problème n’est peut être pas tout de suite évident, alors détaillons la chaîne.

Lorsque la lecture se termine un événement est créé, il est ensuite envoyé dans un callback interne (pour que libplayer sache que la lecture est terminée) et se callback envoi l’événement à un callback externe (pour que l’utilisateur de la lib soit aussi informé). Si depuis ce callback interne, un nouveau player_playback_start() serait amorcé, le callback serait bloqué tant que la lecture n’a pas réellement commencé. Et dès que celle-ci démarre vraiment, la fonction est relâchée. De ce fait, le callback interne resterait bloqué par le player_playback_start() pendant tout ce temps (si c’est un flux réseau, ça peut prendre quelques secondes). Les événements qui arriveraient entre le EOF et la fin du nouveau start s’empileraient et ne seraient pas envoyés. Ainsi des événements périmés arriveraient à l’utilisateur après le start et ce n’est pas acceptable.

En réalité c’est encore un peu plus compliqué que ça, mais il est inutile d’aller dans les détails.

Le superviseur et le gestionnaire d’événement de libplayer permettent donc de gérer se problème. Lorsqu’un événement est envoyé, il est empilé dans un FIFO. Les événements sont prioritaires et le superviseur est désactivé lorsque l’un d’eux survient. Ainsi il ne peut jamais y avoir d’événements périmés. La deuxième chose concerne le superviseur en lui même. Il met à disposition deux moyens pour exécuter une commande tel que le player_playback_start() par exemple. Le premier est le mode WAIT, et le second NO_WAIT. La différence est que le premier est utilisé pour toutes les fonctions publiques. Cela veut dire que tant que la fonction n’est pas terminée (par exemple tant que le start n’a pas réellement commencé), l’utilisateur est bloqué. Le second mode permet de lancer une fonction mais sans aucune attente. Par exemple le start serait empilé dans le FIFO du superviseur mais n’attendrait pas. Ce mode n’est pas autorisé depuis l’API publique pour plusieurs raisons, dont une très importante. Lorsque l’utilisateur utilise la bibliothèque il doit être sûr que la commande soit effectivement exécutée afin d’avoir un comportement “normal” donc comme s’il n’y avait pas de parallélisme. Quand vous faites par exemple un printf(), vous voulez que celui-ci s’exécute à ce moment là et pas 2 secondes plus tard. Sinon dans le cas de libplayer, vous feriez un start, puis autre chose, un stop, get_property, etc,..  Sans savoir quand est-ce que les fonctions seront réellement exécutées. Dit autrement, les fonctions de l’API publique sont toutes synchrones. L’autre mode est asynchrone et demande quelques considérations. Pour les fonctions qui retournent des valeurs par exemple, si l’API publique était asynchrone, il faudrait pouvoir indiquer un callback pour chaque fonction afin d’avoir un moyen de récupérer les valeurs. Cette fonctionnalité n’est pas disponible actuellement.

Mais revenons en à l’automatisation de la lecture. Pour que libplayer puisse passer au morceau suivant sans bloquer le callback du gestionnaire d’événement il doit travailler de manière asynchrone. C’est donc (à l’heure où j’écris ces lignes) le seul endroit où une fonction est utilisée en NO_WAIT (en vérité ce n’est pas tout à fait vrai, mais je vous épargne les autres cas particuliers). Cette fonction se nomme:

  • player_mrl_next_play()
    Les fonctions player_mrl_next() et player_mrl_previous() à disposition dans l’API publique permettent de progresser dans la liste de lecture par rapport à l’ordre d’insertion des mrls. La fonction next_play est bien différente. Elle va tenir compte du “loop” et du “shuffle” et est donc un peu plus sophistiquée.

Je n’ai pas rendu accessible cette fonction depuis l’API publique pour aucune bonne raison. Elle peut très bien être utilisée de manière synchrone. Elle est utilisée en asynchrone dans libplayer pour libérer le callback le plus vite possible et donc pour respecter la bonne marche des événements. Le fait que les événements sont prioritaires au superviseur indique aussi que la fonction ne pourra jamais se terminer tant que le gestionnaire d’événement ne redonne pas la main au superviseur. Une fonction forcée en WAIT provoquerait irrémédiablement un deadlock. Autrement dit ce serait le serpent qui se mort la queue (ou qui se mort le FIFO).

Conclusion

Le but de cet article est de montrer une facette peu connue de libplayer. Et que cette bibliothèque est bien plus qu’une simple couche d’abstraction sur MPlayer, xine, VLC et GStreamer. Les rôles du superviseur et du gestionnaire d’événement ne se limitent pas qu’aux cas d’utilisations exprimés précédemment. Il y a de nombreuses autres raisons mais ce n’est pas l’objectif de cet article que d’en parler en détail ici.

A bientôt,

Mathieu SCHROETER