Linux Embedded

Le blog des technologies libres et embarquées

Playback vidéo avec Qt+Gstreamer sur i.MX8

Problématique

Dans cet article, nous allons écrire une application de playback vidéo en Qt pour la carte Nitrogen 8M de Boundary device. Voici mon environnement : 

Nitrogen 8M de Boundary devices

Yocto Thud avec le BSP fournis par Boundary devices

Pour lire un fichier vidéo, nous pouvons généralement utiliser gplay-1.0, sous weston (un compositeur Wayland) ou sans compositeur. Gplay-1.0 (FSL_GPLAY) est une application développée par la communauté Gstreamer pour faire du playback vidéo. Cette application fonctionne parfaitement. Cependant, dans le cas où l’on doit développer nos propres applications en QT avec du rendu vidéo, nous devons embarquer notre propre lecteur vidéo qui s’affichera sur une de nos fenêtres. Cela implique d’intégrer le flux Vidéo dans QT. 

L’approche la plus évidente consiste à développer un lecteur de vidéo avec le paquet “Qtmultimedia”. Cependant le résultat n’est pas satisfaisant. En effet, la vidéo ne se joue qu’à environ une image par seconde avec énormément de perte. Le backend de Qtmultimedia est bien gstreamer mais, comme on peut le remarquer après étude, il utilise un sink non optimisé nommé qgstvideorenderersink qui n’utilise que les ressources du CPU, contrairement à gplay-1.0 qui utilise le GPU pour le calcul et l’affichage.

QGstVideoRendererSink du pipeline qtmultimedia

Cette solution n’est donc pas utilisable car le CPU i.MX8 n’a pas une puissance suffisante pour traiter seul de la vidéo haute résolution. Pour solutionner ce problème, nous devons changer le dernier “sink” pour qu’il utilise les ressources de GPU.

Solution : qtglsink

Dans le paquet gstreamer-plugins-good, nous avons trouvé un sink nommé “qtsink” (le nom de l’exemple est qmlsink). En bref, comme présenté dans le pipeline ci-dessous, on a un élément “glupload” qui va recevoir les signaux vidéo, et les pousser vers qmlsink pour affichage.  Derrière, ce sont les fonctions OpenGL qui sont utilsées et l’implémentation i.MX8 utilise le GPU pour faire le traitement.

QtSink du pipeline gstreamer

Basé sur l’exemple de videosrctest, nous avons développé une chaîne complète pour lire les flux multimédia. Voici le pipeline provisoire : 

pipeline provisoire

Le concept du programme est assez simple. D’abord, nous prenons les arguments à l’entrée pour déterminer le fichier vidéo à lire. Ensuite, nous créons le pipeline, et nous lui associons le qtsink avec un objet QQuickWindow pour afficher sur l’écran. Nous avons également mis en place quelques objets comme bouton et slider pour gérer le playback.

Programme en détail

Nous avons créé deux classes : Qmlplayer et Setplaying. Dans la première classe, il y a toutes les fonctions pour initialiser le pipeline et contrôler le flux vidéo. L’autre classe Setplaying sert à déclencher le pipeline une fois que la fenêtre est prête.

Etablir le pipeline

Selon le pipeline ci-dessus, nous allons d’abord créer les éléments correspondants : pipeline, uridecodebin, videoconvert/audioconvert, audioresample, volume, alsasink, glupload et qmlglsink.

int
Qmlplayer::qmlplayer_init(const QUrl url)
{
    data.pipeline = gst_pipeline_new (NULL);
    data.decode = gst_element_factory_make ("uridecodebin", NULL);
    g_object_set(data.decode, "uri",url.toEncoded().data(),NULL );

    /***Video Init***/
    data.videoconvert = gst_element_factory_make("videoconvert",NULL);
    // … Pareil pour glupload et qmlglsink

    /***Audio Init***/
    data.audioconvert = gst_element_factory_make("audioconvert",NULL);
    // .. Pareil pour audioresample, volume et alsasink

    gst_bin_add_many (GST_BIN (data.pipeline), data.decode, data.videoconvert, data.glupload, \
                     data.qmlglsink, data.audioconvert, data.audioresample, data.volume,\
                     data.alsasink, NULL);
    ...
}

Ensuite, nous allons établir les liaisons entre les éléments. Certains éléments n’ont que des pads statiques (always pad). On applique alors une fonction gst_element_link_many() pour les lier ensemble. D’autres éléments possèdent des pads qui demandent une liaison dynamique (sometimes pad et request pad). Pour ces derniers, on doit utiliser une fonction de callback pour gérer cela au runtime.

/****Video static link****/
if (!gst_element_link_many(data.videoconvert,data.glupload,data.qmlglsink,NULL))
{
    g_printerr ("video elements could not be linked.\n");
    gst_object_unref (data.pipeline);
    return -1;
}
// … Pareil pour audioconvert, audioresample, volume et alsasink

/**** Link dynamic pad ****/
g_signal_connect(data.decode,"pad-added",G_CALLBACK(pad_added_handler),&data);

Dans la fonction Callback, on doit identifier la source et le type de demande, pour ensuite créer et lier les pads correspondants. Nous avons deux liaisons à établir dynamiquement, une entre uridecodebin et videoconvert, l’autre entre uridecodebin et audioconvert. Nous allons d’abord créer des pads statiques, et après la vérification des informations du nouveau pad, nous allons utiliser gst_pad_link() pour faire la liaison.

void
Qmlplayer::pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data)
{
    GstPad *videosink_pad = gst_element_get_static_pad(data->videoconvert,"videosink");
    GstPad *audiosink_pad = gst_element_get_static_pad(data->audioconvert,"audiosink");
    GstPadLinkReturn ret;

    GstCaps * new_pad_caps = NULL;
    GstStructure * new_pad_struct = NULL;
    const gchar *new_pad_type = NULL;

    if (gst_pad_is_linked(videosink_pad) && gst_pad_is_linked(audiosink_pad)) {
        g_print ("We are already linked. \n");
        goto exit;
    }

    new_pad_caps = gst_pad_get_current_caps(new_pad);
    new_pad_struct = gst_caps_get_structure(new_pad_caps,0);
    new_pad_type = gst_structure_get_name(new_pad_struct);

    if (g_str_has_prefix(new_pad_type,"video/x-raw")) {
        videosink_pad = gst_element_get_compatible_pad (data->videoconvert, new_pad, NULL);
        ret = gst_pad_link(new_pad, videosink_pad);
        // ...
    } else if (g_str_has_prefix(new_pad_type,"audio/x-raw")) {
        videosink_pad = gst_element_get_compatible_pad (data->audioconvert, new_pad, NULL);
        ret = gst_pad_link(new_pad, videosink_pad);
        // ...
    } else {
        g_print("It has type '%s' which is not raw video nor raw audio. Ignoring. \n",new_pad_type);
    }
exit: 
// ...
}

Après avoir lié tous les élément, nous allons initialiser une instance runnable Setplaying. Dans la fonction main(), nous allons d’abord faire appel au fichier qml pour accéder à l’objet videoItem, puis nous allons lui donner la vidéo à afficher. Lorsque toutes ces préparations sont prêtes, on va lancer l’instance Setplaying.

QQmlApplicationEngine engine(QUrl("qrc:///main.qml"));
QQuickItem *videoItem;
QQuickWindow *rootObject;

/* find and set the videoItem on the sink */
rootObject = static_cast<QQuickWindow *> (engine.rootObjects().first());
videoItem = rootObject->findChild<QQuickItem *> ("videoItem");
g_assert (videoItem);
g_object_set(qplayer->data.qmlglsink, "widget", videoItem, NULL);
rootObject->scheduleRenderJob (qplayer->play, QQuickWindow::BeforeSynchronizingStage);

qplayer->set_qmlobject(rootObject);

ret = app.exec(); 

Dans Setplaying, on va mettre notre vidéo en mode pause, et on attend un clic de souris pour déclencher le playback.

Contrôler le flux

Afin de contrôler le flux vidéo, nous aurons besoin des fonctions de Gstreamer suivantes :

gst_element_get_state() // Obtenir l’état de vidéo
gst_element_set_state() // Changer l’état de vidéo
gst_element_query_duration() // Obtenir la longueur du flux vidéo en nanoseconde
gst_element_query_position() //Obtenir la position actuelle du flux vidéo en nanoseconde
gst_element_seek() // Changer la position du flux vidéo
gst_debug_bin_to_dot_file_with_ts() // Sortir la pipeline si l’on est en mode de debug
…

On peut associer les comportements de l’interface utilisateur et ces fonctions en créant des signaux et des slots. Voici un exemple concernant la position du flux vidéo : 

// Dans le fichier qml
Text {
    id: position
    objectName: "position"
    // ...
    signal getPosition()

    Timer{
        //...
        onTriggered: position.getPosition()
    }
} 

Dans le fichier qml, on crée un signal “getPosition”. Ensuite, on associe ce signal avec un slot dans l’objet Qmlplayer, où on va utiliser la fonction “gst_element_query_position()” et “setProperty()” afin de changer l’affichage sur l’écran (le timer et le slider).

// Dans Objet C++ Qmlplayer
// ...
void
Qmlplayer::set_qmlobject(QQuickWindow *rootObject)
{
    this->object = rootObject;
    QQuickItem* position = rootObject->findChild<QQuickItem *> ("position");
    // ...      connect(position,SIGNAL(getPosition()),this,SLOT(getPosition()));
    // ...
}
// …
gint64
Qmlplayer::getPosition()
{
    GstElement* pipeline = this->data.pipeline ? static_cast<GstElement *> (gst_object_ref (this->data.pipeline)) : NULL;
    gint64 pos;
    GstState current_state;
    if(pipeline) {
        // ...
        gst_element_query_position(pipeline,GST_FORMAT_TIME,&pos); //time in nanosecond
        QQuickItem* item = this->object->findChild<QQuickItem *> ("rootItem");
        QVariant v((unsigned long long)(pos/1000000));
        item->setProperty("pos",v);
        return pos;
    }
    else {
        g_print("NULL pipeline!\n");
        return 0;
    }
} 

Conclusion

Dans cet article, nous avons créé un mini-lecteur de vidéo avec Qt et Gstreamer. Vu que le SoC i.MX8 possède un GPU, il est indispensable de l’exploiter pour gagner de la performance. Par contre, le qgstvideorenderersink dans le paquet Qtmultimedia ne se base pas sur OpenGL. C’est pour cette raison qu’il est nécessaire d’écrire nous même notre pipeline. L’avantage de cette solution est que nous sommes capables d’obtenir une application adaptée à nos besoins utilisant le moins de ressources possibles. En revanche, si nous voulons avoir plus de fonctionnalités vidéo (seek, pause, accélération…) dans notre application, ce sera un processus plutôt fastidieux qui nécessitera d’étudier les fonctions existantes de QtMultimédia et de les re-coder de manière à ce qu’elles soient interfaçables avec notre élément sink. En attendant le support de Qtmultimedia, c’est cependant une solution temporaire mais viable pour embarquer un lecteur vidéo dans une application QT. 

Référence : Code source de Qmlplayer : https://github.com/Openwide-Ingenierie/imx8-qmlplayer