Linux Embedded

Le blog des technologies libres et embarquées

Renforcez les capacités de Gstreamer avec vos propres plugins

 

Renforcez les capacités de Gstreamer avec vos propres plugins

Il était une fois Gstreamer

Gstreamer est un framework multimédia bien connu dans le monde Linux aujourd'hui. Il sert de base à de nombreuses applications comme le lecteur vidéo Totem par exemple. Il peut être comparé en termes de fonctionnalités à l'API Direct3D présent sous Windows. Gstreamer offre au programmeur un moyen simple d'accéder et de manipuler un contenu vidéo ou audio, via des modules pour réaliser de l'acquisition, de l'encodage/décodage, de l'enregistrement, de la diffusion... Il se base sur des briques très répandues sous Linux comme v4l, ffmpeg/libav ou alsa.

Actuellement, deux versions sont largement diffusées. D'abord, l'ancienne branche gstreamer 0.10, encore utilisée dans de nombreuses applications. La nouvelle révision 1.x (1.4 à l'heure actuelle), est conseillée pour les nouveaux développements. Elle est incompatible avec la 0.10 et apporte des refontes dans des parties clés de gstreamer. Il est toutefois possible d'installer les deux versions en parallèle sur les distributions Linux.

Gstreamer: concepts généraux et terminologie

Le pipeline d'éléments

Le fonctionnement d'une application gstreamer repose sur la création d'un pipeline regroupant différents éléments gstreamer. Un élément gstreamer correspond à une des étapes d'une chaîne de traitement multimédia. Par exemple, il existe un élément pour acquérir des trames vidéos depuis un périphérique V4L, un autre élément permet d'encoder le flux vidéo en h264 et un dernier élément permet de réaliser l'enregistrement des trames obtenues dans un fichier. Il existe aussi des éléments au but plus générique, comme le tee qui permet de dupliquer le flux entrant, ou la queue pour répartir les éléments du pipelines dans différents threads..

Lorsque nos éléments sont créés et ajoutés au pipeline, on doit ensuite les lier entre eux. Chaque élément est attaché au suivant de la chaîne grâce aux pads. Un élément dispose d'un ou plusieurs pads en entrée et/ou en sortie. En entrée d'élément, un pad est appelé sink et en sortie src. Chaque pad peut placer des restrictions sur le format de données qu'il accepte ou produit. On utilise pour cela les caps (raccourci de capabilities, ou capacités), qui permettent de décrire la résolution des trames vidéos produites. Pour attacher deux pads ensemble, il faut qu'ils partagent un format de données commun pour que le pipeline puisse s'exécuter correctement. Les éléments liés entre eux se mettent d'accord sur le format utilisé lors de la phase de négociation.

Les conteneurs

Chaque élément peut, au lieu d'être ajouté au pipeline directement, être intégré dans un élément particulier, le bin. Le but d'un bin est de regrouper différents éléments pour masquer leurs fonctionnements internes et fournir un élément unique au développeur qui servira d'interface. Par exemple, le bin rtspsrc (source RTSP) permet de se connecter à un serveur de streaming rtsp. Ce bin est constitué de plusieurs éléments, avec par exemple un élément pour se connecter en tcp, un autre pour récupérer les paquets udp et un dernier pour contrôler la mise en tampon des données reçues. Le bin peut être ajouté lui même au pipeline et connecté aux autres éléments. Il permet donc de rajouter une couche d'abstraction pour simplifier l'architecture d'un pipeline.

En fait, un pipeline est lui même un bin avec des fonctionnalités en plus, comme la gestion d'une horloge pour synchroniser les données et du bus pour le passage de message entre éléments.

La lecture du pipeline

Lorsque notre pipeline est assemblé et les éléments qui le constituent liés ensemble, le pipeline peut démarrer son traitement. Pour cela, on va modifier l'état du pipeline et des éléments. Par défaut le pipeline est en état READY. On commence le traitement en changeant l'état du pipeline à PAUSED puis à PLAYING. Pour cela, le pipeline force tous ses éléments à passer par les mêmes changements d'état. Si un élément bloque, le pipeline est interrompu et le traitement ne peut pas s'exécuter. Une fois notre travail terminé, il faut passer le pipeline à l'état NULL, ce qui entraîne la suppression des éléments contenus.

C, Glib et GObject

Gstreamer est codé en C, avec l'aide de la Glib. La Glib est une bibliothèque C très complète avec, par exemple, la gestion des threads, la programmation événementielle, les listes chaînées ou encore le comptage de références pour l'allocation de ressources. La Glib est notamment utilisée par toutes les applications du bureau GNOME, car elle est au coeur même du toolkit graphique GTK. En plus de la Glib, Gstreamer utilise aussi la bibliothèque GObject, complément à la Glib qui permet d'utiliser l'ensemble des concepts de la programmation objet en C. Cela permet donc de créer des classes et des instances, d'utiliser les principes de l'héritage, de l'encapsulation des données,... Cependant, le développement reste plus fastidieux que l'utilisation d'un langage objet natif comme le C++, et certains développeurs critiquent la complexité et la taille du code boilerplate nécessaire.

Gstreamer utilise fortement ce principe de programmation objet. Ainsi, chaque élément créé est une instance de la structure GstElement (équivalent d'une classe en C++). Les éléments sont configurés via leurs propriétés (équivalent des membres d'une classe en C++). Pour les fonctions membres, on utilise des pointeurs de fonctions avec en premier argument un pointeur vers l'instance de la structure. La notion d'héritage est très présente, par exemple avec la hiérarchie de classes GstObject <- GstElement <- GstBin <- GstPipeline.

Toi, tu creuses

Chaque élément gstreamer est contenu dans un plugin gstreamer. Les plugins sont séparés dans des dépôts nommés good, bad et ugly, d'après le western de Sergio Leone. La répartition est faîte selon la qualité du code et du support, et les éventuels problèmes de redistributions dus aux licences et brevets. N'hésitez pas à installer l'ensemble des modules pour accéder rapidement à toutes les possibilités de gstreamer.

Un plugin est présent sous la forme d'une bibliothèque dynamique. Lorsque l'on démarre une application gstreamer, seul le coeur du framework est chargé avec les fonctionnalités de base. Les plugins sont ensuite chargés dynamiquement par gstreamer selon les besoins, ce qui permet de réduire l'empreinte mémoire de l'application. Lorsque l'instanciation du plugin est réussi, le développeur peut utiliser le ou les éléments qu'il contient.

Le chargement des plugins est géré automatiquement par gstreamer sans intervention du développeur sur simple demande d'un nouvel élément. Pour cela, gstreamer maintient un registre listant les plugins installés sur le système et les éléments qu'ils fournissent.

Quelques préparatifs avant la bataille

Pour ce tutoriel, nous allons écrire un plugin contenant un élément qui traite un flux vidéo et transforme les trames couleurs reçues en trames monochromes. Cet élément disposera donc d'un pad en entrée qui acceptera une vidéo au format raw et un pad en sortie au même format.

Génération du code boilerplate

Comme expliqué précédemment, l'utilisation de GObject requiert l'écriture d'un squelette bien défini. Pour simplifier le travail, Gstreamer propose un outil permettant de générer le code de base pour un plugin. Pour cela, il faut cloner le dépôt contenant les plugins bad, à l'adresse suivante : git://anongit.freedesktop.org/gstreamer/gst-plugins-bad. Les scripts sont situés dans le dossier tools.

Tout d'abord, plaçons nous dans notre dossier $HOME, puis utilisons le script gst-project-maker.

$ <path_to_gst_plugins_bad>/tools/gst-project-maker monochrome_filter

Cela créé les fichiers nécessaires dans le dossier gst-monochrome-filter pour construire un projet avec autotools. On retrouve notamment les fichiers configure.ac et Makefile.am, ainsi qu'un script autogen.sh qui permet de compiler l'ensemble des fichiers sources pour générer une bibliothèque dynamique.

Ensuite, il faut se rendre dans le dossier plugins dans le répertoire du projet. La lecture du fichier gstMonochromeFilter.c indique que l'on doit désormais utiliser l'outil gst-element-maker. Ce script prend deux arguments. Le premier est le nom de votre nouveau plugin : ici, MonochroneFilter. Le deuxième est le nom du template utilisé comme base de l'élément. Les templates sont disponibles dans le dossier tools/element-templates tools du dépôt gst-plugins-bad. Le template peut notamment décrire le type de données traitées (vidéo, audio, ...) et la présence de pads en entrée et/ou en sortie de l'élément. Le template sert également à générer le code GObject et Gstreamer minimum requis. Les exemples les plus génériques sont gobject (code simple pour un GObject) puis element (code simple pour un élément gstreamer). Bien sûr, ce ne sont que des bases pour génération de code, et on est libre de modifier par la suite le code source si besoin. Pour notre exemple, on utilisera le template videofilter.

$ <path_to_gst_plugin_bad>/tools/gst-element-maker monochrome_filter videofilter

Le code généré de gstmonochromefilter.c comporte pas moins de 262 lignes! Remercions les développeurs pour ces scripts qui nous évitent un travail assez rébarbatif. Le template videofilter est en fait basé sur l'élément abstrait GstVideoFilter, lui même basé sur GstBaseTransform. Par défaut, il propose un pad en entrée et un pad en sortie traitant de la vidéo, le format sur ces deux pads est le même.

Compilation en cours...

Comme notre élément est basé sur videofilter, il faut linker avec libgstvideo.so. Pour cela, on complète la directive PKG_CHECK_MODULES du configure.ac et on rajoute le module gstreamer-video-1.0.

PKG_CHECK_MODULES(GST, [
  gstreamer-1.0 >= $GST_REQUIRED
  gstreamer-base-1.0 >= $GST_REQUIRED
  gstreamer-controller-1.0 >= $GST_REQUIRED
  gstreamer-video-1.0 >= $GST_REQUIRED
], [

Le projet est presque prêt, il ne reste plus qu'à supprimer le code qui redéfinit le plugin dans gstmonochromefilter.c (ligne 240 à 262). On peut aussi enlever la définition de la fonction plugin_init qui ne sert plus à rien dans ce même fichier. Enfin, on peut supprimer le fichier plugins/libgstmonochromefilter.so généré par gst-element-maker qui ne sera pas reconstruit par les règles Makefile.

Pour générer notre plugin, on exécute autogen.sh puis make.

$ autogen.sh
$ make.

Le résultat de la compilation est une bibliothèque dynamique ./plugins/.libs/libgstmonochromefilter.so.

MonochromeFilter, présentez vous !

Des FIXME sont placés dans le code pour que le développeur complète certaines parties, comme la description du plugin, les formats pris en charge par les caps et les metadata du plugin. Les metadata contiennent le nom de l'élément, le nom de du développeur et son adresse e-mail ainsi qu'une autre description du rôle de l'élément. Cela permet à gstreamer d'identifier le nouveau plugin dans son registre.

Ces données peuvent être affichées avec l'outil gst-inspect :

$ gst-inspect-1.0 ./plugins/.libs/libgstmonochromefilter.so
Plugin Details:
  Name                     monochromefilter
  Description              FIXME Template plugin
  Filename                 ./plugins/.libs/libgstmonochromefilter.so
  Version                  1.0.0
  License                  LGPL
  Source module            gst-monochrome_filter
  Binary package           GStreamer
  Origin URL               http://gstreamer.net/

  monochromefilter: FIXME Long name

  1 features:
  +-- 1 elements

Les informations relatives au plugin sont listées ici, avec son nom, sa licence,... Ce plugin contient un seul élément, appelé lui-aussi monochromefilter. Consultons maintenant les détails propre à l'élément lui-même. Pour cela, nous utilisons la variable d'environnement GST_PLUGIN_PATH qui permet de spécifier des répertoires non standards qui contiennent des plugins gstreamer (le fonctionnement est similaire avec la variable LD_LIBRARY_PATH utilisée pour chercher les bibliothèques dynamiques du système).

La sortie console est cette fois assez longue, mais contient de très importantes informations. On trouve d'abord le nom de l'élément, son auteur, et le plugin auquel il appartient :

$ GST_PLUGIN_PATH=./plugins/.libs gst-inspect-1.0 monochromefilter
Factory Details:
  Rank                     none (0)
  Long-name                FIXME Long name
  Klass                    Generic
  Description              FIXME Description
  Author                   FIXME <fixme@example.com>

Plugin Details:
  Name                     monochromefilter
  Description              FIXME Template plugin
  Filename                 ./plugins/.libs/libgstmonochromefilter.so
  Version                  1.0.0
  License                  LGPL
  Source module            gst-monochrome_filter
  Binary package           GStreamer
  Origin URL               http://gstreamer.net/

Ensuite, on peut voir la hiérarchie GObject sur laquelle cet élément est basé :

GObject
 +----GInitiallyUnowned
       +----GstObject
             +----GstElement
                   +----GstBaseTransform
                         +----GstVideoFilter
                               +----GstMonochromeFilter

Vient ensuite la description des pads en entrée de l'élément (sink) et en sortie (src), ainsi que le format supporté (vidéo raw dans notre exemple). Le format est décrit sous forme de capabilities gstreamer. Actuellement. on a le choix entre 5 espaces de couleur. La résolution et le framerate sont exprimés par des range, avec dans l'exemple les valeurs maximales supportées par gstreamer. Autre information importante, le mode de disponibilité (availability). Dans notre exemple, nos deux pad sont toujours présent (mode always), mais on peut aussi trouver des éléments dont les pads doivent être explicitement demandés par l'application (mode request).

Pad Templates:
  SINK template: 'sink'
    Availability: Always
    Capabilities:
      video/x-raw
                 format: { I420, Y444, Y42B, UYVY, RGBA }
                  width: [ 1, 2147483647 ]
                 height: [ 1, 2147483647 ]
              framerate: [ 0/1, 2147483647/1 ]

  SRC template: 'src'
    Availability: Always
    Capabilities:
      video/x-raw
                 format: { I420, Y444, Y42B, UYVY, RGBA }
                  width: [ 1, 2147483647 ]
                 height: [ 1, 2147483647 ]
              framerate: [ 0/1, 2147483647/1 ]

Capabilities:
  video/x-raw
             format: { I420, Y444, Y42B, UYVY, RGBA }
              width: [ 1, 2147483647 ]
             height: [ 1, 2147483647 ]
          framerate: [ 0/1, 2147483647/1 ]

Element Flags:
  no flags set

Element Implementation:
  Has change_state() function: gst_element_change_state_func

Element has no clocking capabilities.

Pads:
  SINK: 'sink'
    Implementation:
      Has chainfunc(): gst_base_transform_chain
      Has custom eventfunc(): gst_base_transform_sink_event
      Has custom queryfunc(): gst_base_transform_query
      Has custom iterintlinkfunc(): gst_pad_iterate_internal_links_default
    Pad Template: 'sink'
  SRC: 'src'
    Implementation:
      Has getrangefunc(): gst_base_transform_getrange
      Has custom eventfunc(): gst_base_transform_src_event
      Has custom queryfunc(): gst_base_transform_query
      Has custom iterintlinkfunc(): gst_pad_iterate_internal_links_default
    Pad Template: 'src'

Element has no URI handling capabilities.

Enfin voici la liste des propriétés de l'élément. Ces propriétés sont l'équivalent des membres d'une classe C++ et sont modifiables de l'extérieur pour configurer le comportement de l'élément. Pour l'instant, trois propriétés communes à tout élément gstreamer sont disponibles. Nous sommes libre de rajouter par la suite nos propres propriétés par la suite.

Element Properties:
  name                : The name of the object
                        flags: accès en lecture, accès en écriture
                        String. Default: "monochromefilter0"
  parent              : The parent of the object
                        flags: accès en lecture, accès en écriture
                        Object of type "GstObject"
  qos                 : Handle Quality-of-Service events
                        flags: accès en lecture, accès en écriture
                        Boolean. Default: true

Écritude du plugin de A à Z

Enfin du code

Il est temps désormais de passer au code.

Le premier fichier est gstmonochromefilterplugin.c. Ce fichier est assez succint et ne concerne que les métadonnées du plugin, à compléter par le développeur.

Les deux fichiers restants sont propres à l'élément MonochromeFilter. L'en-tête gstmonochromefilter.h contient principalement la définition des structures GObject. On trouve une structure pour la classe GstMonochromeFilterClass, qui contient l'équivalent des membres statiques en C++ et les pointeurs de fonctions, et la structure GstMonochromeFilter, utilisée pour créer des instances de l'élément.

struct _GstMonochromeFilter
{
  GstVideoFilter base_monochromefilter;

};

struct _GstMonochromeFilterClass
{
  GstVideoFilterClass base_monochromefilter_class;
};

Le fichier gstmonochromefilter est plus long, mais il n'est pas très difficile à lire. Le premier point que nous allons modifier sont les pads templates qui permettent de définir aisément les formats acceptés en entrée et en sortie d'élément. En bon programmeur fainéant, et aussi pour simplifier l'écriture de notre plugin, nous allons limiter les formats supportés au RGB.

/* pad templates */

#define VIDEO_SRC_CAPS \
    GST_VIDEO_CAPS_MAKE("{ RGB, RGBx, xRGB, RGBA, ARGB }")

#define VIDEO_SINK_CAPS \
    GST_VIDEO_CAPS_MAKE("{ RGB, RGBx, xRGB, RGBA, ARGB }")

Nous avons ensuite les fonctions en charge de l'initialisation de la structure de la classe et de la structure des instances. Seule la première est remplie pour l'instant avec notamment l'ajout des pads avec les formats définis plus haut, quelques metadata et l'assignation des pointeurs de fonctions (équivalent des méthodes en C++). On trouve notamment deux fonctions membres transform_frame et transform_frame_ip. Ce sont ces deux membres qui sont responsable des transformations en monochrome des trames reçues. Pour ce plugin, les buffers reçus en entrée auront la même taille que les buffers de sortie, aussi on pourra faire les transformations directement sur les données d'entrée (transformation in place). Nous pouvons donc supprimer le membre transform_frame et la fonction associée gst_monochrome_filter_transform_frame, pour n'utiliser que transform_frame_ip. Le format négocié sur le pad src sera aussi utilisé sur le pad sink. Pour changer ce comportement, il faudrait surcharger la fonction transform_caps, héritée de GstBaseTransform.

static void
gst_monochrome_filter_class_init (GstMonochromeFilterClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstBaseTransformClass *base_transform_class = GST_BASE_TRANSFORM_CLASS (klass);
  GstVideoFilterClass *video_filter_class = GST_VIDEO_FILTER_CLASS (klass);

  /* Setting up pads and setting metadata should be moved to
     base_class_init if you intend to subclass this class. */
  gst_element_class_add_pad_template (GST_ELEMENT_CLASS(klass),
      gst_pad_template_new ("src", GST_PAD_SRC, GST_PAD_ALWAYS,
        gst_caps_from_string (VIDEO_SRC_CAPS)));
  gst_element_class_add_pad_template (GST_ELEMENT_CLASS(klass),
      gst_pad_template_new ("sink", GST_PAD_SINK, GST_PAD_ALWAYS,
        gst_caps_from_string (VIDEO_SINK_CAPS)));

  gst_element_class_set_static_metadata (GST_ELEMENT_CLASS(klass),
      "Monochrome color element", "Generic", "Convert video frame in monochrome",
      "Amaury Denoyelle <amaury.denoyelle@openwide.fr>");

  gobject_class->set_property = gst_monochrome_filter_set_property;
  gobject_class->get_property = gst_monochrome_filter_get_property;
  gobject_class->dispose = gst_monochrome_filter_dispose;
  gobject_class->finalize = gst_monochrome_filter_finalize;
  base_transform_class->start = GST_DEBUG_FUNCPTR (gst_monochrome_filter_start);
  base_transform_class->stop = GST_DEBUG_FUNCPTR (gst_monochrome_filter_stop);
  video_filter_class->set_info = GST_DEBUG_FUNCPTR (gst_monochrome_filter_set_info);
  video_filter_class->transform_frame_ip = GST_DEBUG_FUNCPTR (gst_monochrome_filter_transform_frame_ip);

}

La suite du fichier contient les implémentations des fonctions assignées à la structure GstMonochromeFilterClass. Les fonctions gst_monochrome_filter_set_property et gst_monochrome_filter_get_property remplissent le rôle des accesseurs C++ pour les membres. Viennent ensuite des fonctions pour libérer les ressources : gst_monochrome_filter_dispose intervient lorsqu'un élément MonochroneFilter est détruit et peut donc être appelée plusieurs fois dans un même programme, alors que gst_monochrome_filter_finalize est exécutée une seule fois à la toute fin du programme lorsque le plugin MonochroneFilter est déchargé.

Les fonctions gst_monochrome_filter_start et gst_monochrome_filter_stop sont appelées respectivement lorsque le pipeline ordonne le démarrage ou l'arrêt de l'élément. Lorsque les éléments se sont mis d'accord sur le format d'échange des données, gst_monochrome_filter_set_info est exécutée avec en paramètre des caps gstreamer décrivant les formats aux pads d'entrée et de sortie (utile pour les éléments qui traitent les données différemment selon les formats acceptés). Enfin, on trouve la fonction responsable du traitement vidéo gst_monochrome_filter_transform_frame_ip.

Premiers tests

Grâce aux scripts de génération de code, notre nouvel élément peut déjà être ajouté à un pipeline. Si on utilise notre plugin dans l'état actuel, le pipeline gstreamer s'exécute correctement, mais notre élément ne modifie pas le flux vidéo et se contente de transmettre tel quel les buffers reçus. Testons cela avec l'utilitaire gst-launch qui permet de construire un pipeline en ligne de commande.

$ GST_PLUGIN_PATH=./plugins/.libs gst-launch-1.0 videotestsrc ! video/x-raw,width=640,height=480,framerate=25/1 ! videoconvert ! monochromefilter ! videoconvert ! autovideosink

Ce pipeline utilise l'élément de test videotestsrc qui permet d'afficher une mire. Chaque élément est séparé par un point d'exclamation. On impose la résolution et le framerate du flux à l'aide d'un capsfilter entre deux éléments. Les deux éléments videoconvert permettent de faire des conversions entre espaces de couleurs si cela est requis, car monochromefilter impose le format RGBA en entrée et en sortie. Enfin, autovideosink est responsable de l'affichage des trames reçues.

Si on souhaite tester avec un périphérique v4l comme source, c'est très simple. Il suffit de remplacer videotestsrc par l'élément v4l2src, et de définir sa propriété device au périphérique vidéo (pour rappel, on peut utiliser gst-inspect pour examiner les propriétés d'un élément) :

$ GST_PLUGIN_PATH=./plugins/.libs gst-launch-1.0 v4l2src device=/dev/video0 ! video/x-raw,width=640,height=480,framerate=25/1 ! videoconvert ! monochromefilter ! videoconvert ! autovideosink

Soyons verbeux

Enfin, très utile pour débugger le comportement de notre élément, on peut définir la variable d'environnement GST_DEBUG. Chaque élément définit un tag pour réduire la taille des logs. Dans le fichier gstmonochromefilter.c, cela se réfère aux lignes suivantes :

GST_DEBUG_CATEGORY_STATIC (gst_monochrome_filter_debug_category);
#define GST_CAT_DEFAULT gst_monochrome_filter_debug_category

Pour chaque fonction, on peut observer un appel à la macro GST_DEBUG_OBJECT qui écrit un trace. Aussi, on définit notre niveau de log au niveau DEBUG (5) ainsi :

$ export GST_DEBUG=monochromefilter:5

Lorsque le pipeline est relancé, on peut maintenant suivre à la trace chaque appel de fonctions pour notre élément.

Conversion en niveaux de gris

Pour assurer le traitements du flux vidéo, c'est la fonction gst_monochrome_filter_transform_frame_ip qui est exécutée. Cette fonction prend en premier argument un pointeur sur l'élément lui même (équivalent du pointeur this en C++). Celui-ci est de type GstVideoFilter car cette fonction est héritée de l'élément générique VideoFilter. En seconde position, on trouve la trame vidéo à traiter. Les changements sont faits directement dans le buffer reçu et sa taille doit rester inchangée pour le bon fonctionnement du pipeline.

Pour convertir en niveau de gris, on choisit de faire la somme des trois pixels de couleurs rouge, vert et bleu et on divise par trois le résultat. Aucun coefficient n'est donné selon le composant du pixel, aussi l'image manquera peut-être d'un peu de contraste aux yeux des amateurs d'effets noir et blanc... Pour se déplacer au sein du buffer, on utilise au maximum les macros de la librairie gstreamer-video, ce qui permet de supporter les différents formats RGB.

static GstFlowReturn
gst_monochrome_filter_transform_frame_ip (GstVideoFilter * filter, GstVideoFrame * frame)
{
  GstMonochromeFilter *monochromefilter = GST_MONOCHROME_FILTER (filter);

  GST_DEBUG_OBJECT (monochromefilter, "transform_frame_ip");

  gint r, g, b, gray;

  gint width, height;
  gint i, j;
  gint row_padding;

  guint8 rgb_offsets[3];
  rgb_offsets[0] = GST_VIDEO_FRAME_COMP_OFFSET (frame, GST_VIDEO_COMP_R);
  rgb_offsets[1] = GST_VIDEO_FRAME_COMP_OFFSET (frame, GST_VIDEO_COMP_G);
  rgb_offsets[2] = GST_VIDEO_FRAME_COMP_OFFSET (frame, GST_VIDEO_COMP_B);

  guint8 *data;

  width = GST_VIDEO_FRAME_WIDTH(frame);
  height = GST_VIDEO_FRAME_HEIGHT(frame);

  data = GST_VIDEO_FRAME_PLANE_DATA(frame, 0);

  row_padding = GST_VIDEO_FRAME_PLANE_STRIDE(frame, 0) - width * GST_VIDEO_FRAME_COMP_PSTRIDE(frame, 0);

  for (i = 0; i < height; ++i) {
    for (j = 0; j < width; ++j) {
      r = data[rgb_offsets[0]];
      g = data[rgb_offsets[1]];
      b = data[rgb_offsets[2]];

      gray = (r + g + b) / 3;

      data[rgb_offsets[0]] = gray;
      data[rgb_offsets[1]] = gray;
      data[rgb_offsets[2]] = gray;

      data += GST_VIDEO_FRAME_COMP_PSTRIDE(frame, 0);
    }

    data += row_padding;
  }

  return GST_FLOW_OK;
}

Configurer le comportement de l'élément via une propriété

Nous sommes maintenant capable d'afficher une belle vidéo en niveau de gris. Pour compléter ce nouvel élément, nous allons laisser le choix au développeur de conserver cet affichage ou de filter par couleur rouge, vert ou bleu en ne conservant que le canal de couleur souhaité. Pour cela, nous allons d'abord ajouter une propriété à l'élément pour configurer le mode de filtrage.

Commençons donc par ajouter cette nouvelle propriété. Cela met en oeuvre des principes généraux de GObject. Tout d'abord, définition une énumération pour les différentes valeurs supportées, puis ajoutons un champ à la structure GstMonochromeFilter de l'en-tête :

enum FilterMode {
  GRAY_FILTER = 0,
  RED_FILTER,
  GREEN_FILTER,
  BLUE_FILTER,
};

struct _GstMonochromeFilter
{
  GstVideoFilter base_monochromefilter;

  enum FilterMode filter_mode;
};

Passons ensuite au fichier gstmonochromefilter.c. On complète l'énumération qui liste les index des différentes propriétés.

enum
{
  PROP_0,
  PROP_FILTER_MODE,
};

On définit ensuite une fonction qui va permettre d'associer une chaîne de caractères à l'énumération FilterMode. Ainsi, on pourra sur la ligne de commande utiliser des valeurs explicites pour définir la propriété.

#define TYPE_FILTER_MODE (filter_mode_get_type ())
static GType
filter_mode_get_type (void)
{
  static GType filter_mode = 0;

  if (!filter_mode) {
    static const GEnumValue modes[] = {
      {GRAY_FILTER, "Grayscale image", "gray"},
      {RED_FILTER,  "Red-tone image", "red"},
      {GREEN_FILTER,  "Green-tone image", "green"},
      {BLUE_FILTER,  "Blue-tone image", "blue"},

      {0, NULL, NULL}
    };

    filter_mode = g_enum_register_static ("Filter_Mode", modes);
  }

  return filter_mode;
}

Une propriété doit toujours avoir une valeur par défaut donnée explicitement. Pour cela, on utilise une macro :

#define DEFAULT_FILTER_MODE GRAY_FILTER

On modifie ensuite le "constructeur" de la classe gst_monochrome_filter_class_init pour installer notre propriété. On donne notamment en argument l'index de la propriété (PROP_FILTER_MODE), un commentaire pour documentation, la valeur par défaut, et des flags pour notamment autoriser la modification de la propriété.

g_object_class_install_property (gobject_class, PROP_FILTER_MODE,
    g_param_spec_enum("filter", "Video filter",
    "Color filter to apply",
    TYPE_FILTER_MODE, DEFAULT_FILTER_MODE,
    G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

Il faut désormais modifier les fonctions qui jouent le rôle des accesseurs des propriétés pour pouvoir définir une nouvelle valeur (set) ou récupérer la valeur actuelle (get).

void
gst_monochrome_filter_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  GstMonochromeFilter *monochromefilter = GST_MONOCHROME_FILTER (object);

  GST_DEBUG_OBJECT (monochromefilter, "set_property");

  switch (property_id) {
    case PROP_FILTER_MODE:
      monochromefilter->filter_mode = g_value_get_enum (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

void
gst_monochrome_filter_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  GstMonochromeFilter *monochromefilter = GST_MONOCHROME_FILTER (object);

  GST_DEBUG_OBJECT (monochromefilter, "get_property");

  switch (property_id) {
    case PROP_FILTER_MODE:
      g_value_set_enum (value, monochromefilter->filter_mode);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

On peut déjà observer l'ajout de cette nouvelle propriété à l'aide de gst-inspect. Si le travail a bien été réalisé, on peut voir apparaître un champ "filter" avec les modes configurés et documentés pour nous.

$ GST_PLUGIN_PATH=./plugins/.libs gst-inspect-1.0 monochromefilter
Factory Details:
  Rank                     none (0)
  Long-name                Monochrome color element
  Klass                    Generic
  Description              Convert video frame in monochrome
  Author                   Amaury Denoyelle <amaury.denoyelle@openwide.fr>

[...]

Element Properties:
  name                : The name of the object
                        flags: accès en lecture, accès en écriture
                        String. Default: "monochromefilter0"
  parent              : The parent of the object
                        flags: accès en lecture, accès en écriture
                        Object of type "GstObject"
  qos                 : Handle Quality-of-Service events
                        flags: accès en lecture, accès en écriture
                        Boolean. Default: true
  filter              : Color filter to apply
                        flags: accès en lecture, accès en écriture
                        Enum "Filter_Mode" Default: 0, "gray"
                           (0): gray             - Grayscale image
                           (1): red              - Red-tone image
                           (2): green            - Green-tone image
                           (3): blue             - Blue-tone image

C'est presque prêt. Il ne nous reste plus qu'à modifier la boucle de traitements des trames vidéos pour utiliser le bon filtre.

  for (i = 0; i < height; ++i) {
    for (j = 0; j < width; ++j) {
      r = data[rgb_offsets[0]];
      g = data[rgb_offsets[1]];
      b = data[rgb_offsets[2]];

      if (monochromefilter->filter_mode == GRAY_FILTER) {
        gray = (r + g + b) / 3;

        data[rgb_offsets[0]] = gray;
        data[rgb_offsets[1]] = gray;
        data[rgb_offsets[2]] = gray;

      } else if (monochromefilter->filter_mode == RED_FILTER) {
        data[rgb_offsets[0]] = r;
        data[rgb_offsets[1]] = 0;
        data[rgb_offsets[2]] = 0;

      } else if (monochromefilter->filter_mode == GREEN_FILTER) {
        data[rgb_offsets[0]] = 0;
        data[rgb_offsets[1]] = g;
        data[rgb_offsets[2]] = 0;

      } else if (monochromefilter->filter_mode == BLUE_FILTER) {
        data[rgb_offsets[0]] = 0;
        data[rgb_offsets[1]] = 0;
        data[rgb_offsets[2]] = b;

      } else {
        GST_WARNING_OBJECT (monochromefilter, "unknown video filter mode, no transformation is done");
      }

      data += GST_VIDEO_FRAME_COMP_PSTRIDE(frame, 0);
    }

    data += row_padding;
  }

C'est terminé. Nous pouvons désormais utiliser différents filtres pour nos pipelines. Par exemple, avec un filtre vert (pour se la jouer vision nocturne) :

$ GST_PLUGIN_PATH=./plugins/.libs gst-launch-1.0 videotestsrc ! video/x-raw,width=640,height=480,framerate=25/1 ! videoconvert ! monochromefilter filter=green ! videoconvert ! autovideosink

Intérêt d'écrire un plugin gstreamer

Écrire un plugin gstreamer n'est pas une tâche aisée. Cela nécessite de comprendre un ensemble de principes propres au framework gstreamer. De plus, développer en GObject peut être vu comme une tâche rébarbative. Heureusement, nous avons à notre disposition des générateurs de codes qui permettent de s'affranchir d'une partie de cette tâche.

Malgré ces points négatifs, l'écriture d'un plugin permet un gain de temps important. Le travail réalisé dans le cadre d'un plugin peut être facilement réutilisé dans différents pipelines gstreamer pour remplir des besoins tout à fait différents. Gstreamer apporte en plus une couche d'abstraction cohérente qui permet de s'affranchir des APIs bas niveau comme V4L, ffmpeg ou encore alsa, qui ont chacune leur mode de fonctionnement et leur philosophie propre..

Aujourd'hui, Gstreamer est devenu une brique importante de l'écosystème Linux. Preuve de ce succès, de nombreux constructeurs comme Freescale ou Texas proposent aujourd'hui des éléments Gstreamer pour accéder aux capacités matériels de leurs systèmes.

Sources

Manuel général pour l'API gstreamer

Manuel pour l'écriture de plugin gstreamer

Le bon, la brute et le truand

 

Edit (01/06/2015) : Ajout de l'initialisation de la variable rgb_offset[3]

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.