La liste de lecture de libplayer

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s