Return home

Pas de son/vidéo ? ... Mais écris le foutu plugin !


10 novembre 2025


N'ayant aucune expérience en Rust à l'époque, j'avais eu l'idée d'écrire une application servant de lecteur de musique permettant aussi de télécharger des musiques. Plusieurs frameworks existent, et mon choix s'est porté sur Tauri, qui est l'équivalent d'Electron mais écrit en Rust.

Le développement se passait plutôt bien jusqu'au moment où j'ai voulu lire un fichier audio. Le plus simple fut de lire le fichier depuis le frontend. Tauri fourni un protocole personnalisé qui permet de faciliter la lecture de fichiers, ce qui nous permet d'avoir :

import { appDataDir, join } from "@tauri-apps/api/path";
import { convertFileSrc } from "@tauri-apps/api/core";

const appDataDirPath = await appDataDir();
const filePath = await join(appDataDirPath, "assets/video.mp4");
const assetUrl = convertFileSrc(filePath);

const video = document.getElementById("my-video");
const source = document.createElement("source");
source.type = "video/mp4";
source.src = assetUrl;
video.appendChild(source);
video.load();

Exemple venant de la documentation de Tauri

Mais quelque chose de particulier se passe, le fichier n'est pas lu et une erreur est affichée :

Unhandled Promise Rejection: NotSupportedError: The operation is not supported

Pouvoir lire un fichier audio/vidéo est quelque chose d'assez courant pour une application, alors je me suis mis à chercher s'il existait une issue GitHub, et ...

oh boy ...

Issue Github

Ce problème semble exister depuis 3 ans, et beaucoup de personnes semblent l'avoir rencontré. On s'attaque donc à du lourd.

Quand un problème se présente, les personnes attendent généralement qu'un Messi arrive et propose un patch, ou bien que quelqu'un propose un moyen de contourner le problème. Pas mal de solutions ont été proposées pour le contourner. Pour les fichiers audio, il suffit de lire le fichier depuis le backend avec une librairie comme rodio (c'est ce que j'ai fait dans mon cas avec mon application). Mais pour les vidéos, cela se complique car il faudrait envoyer chaque frame du backend vers le frontend de manière efficace...

L'une des solutions possibles est de lire le fichier brut et d'en faire un blob (Binary Large Objects) :

  import { readBinaryFile } from "@tauri-apps/api/fs"
  const [urlAudio, setUrlAudio] = useState<string>("")
  return (
    <>
      <Button
        onClick={() => {
          const filePath = "imafile.mp3" // or *.mp4
          void readBinaryFile(filePath)
            .catch((err) => {
              console.error(err)
            })
            .then((res) => {
              const fileBlob = new Blob(
                [res as ArrayBuffer],
                { type: "audio/mpeg" }
              )
              const reader = new FileReader()
              reader.readAsDataURL(fileBlob)
              const url = URL.createObjectURL(fileBlob)
              setUrlAudio(url)
            })
        }}
      >
        Im a button
      </Button>
      <audio controls>
        <source src={urlAudio} type="audio/mpeg" />
      </audio>
    </>
  )

Mais cela atteint ses limites lorsqu'on veut lire des vidéos de grande tailles (> 1Go).

Sinon, l'une des solutions que je trouve délirante est de faire passer la donnée via un serveur HTTP local.

Parmi toutes ces bidouilles, je n'étais pas satisfait, alors j'ai creusé.

WebKitGTK

WebKitGTK est le port de WebKit pour GTK sous Linux.

Derrière Tauri se cachent différents moteurs web intégrés en fonction du système d'exploitation, dont WebKitGTK pour Linux. WebKitGTK dispose d'un bugzilla et après avoir fait quelques recherches, je suis tombé sur un post datant de 2015 (il y a plus de 10 ans !) décrivant le même problème. L'une des personnes a pu écrire un code minimal pour reproduire le problème :

#include <gtk/gtk.h>
#include <webkit2/webkit2.h>

static void uri_scheme_request_cb(
  WebKitURISchemeRequest *request,
  gpointer user_data
)
{
    [...]
    stream = g_file_read(file, NULL, &err);
    if (err == NULL)
    {
        GFileInfo *file_info = g_file_query_info(file, "standard::*",
                                                 G_FILE_QUERY_INFO_NONE,
                                                 NULL, &err);
        if (file_info != NULL)
            stream_length = g_file_info_get_size(file_info);
        else
        {
            g_error("Could not get file info: %s\n", err->message);
            g_error_free(err);
            return;
        }

        webkit_uri_scheme_request_finish(
            request, G_INPUT_STREAM
            (stream), stream_length,
            g_file_info_get_content_type(file_info)
        );
        g_object_unref(stream);
        g_object_unref(file_info);
    }
    else
    {
        webkit_uri_scheme_request_finish_error (request, err);
        g_error_free (err);
    }
}

int main(int argc, char *argv[])
{
    WebKitWebContext *ctx;
    ctx = webkit_web_context_new();
    webkit_web_context_register_uri_scheme(
        ctx, "custom",
        (WebKitURISchemeRequestCallback)uri_scheme_request_cb,
        NULL, NULL
    );

    [...]
    return 0;
}

Ce code ajoute un handler lorsqu'un lien avec custom:// est appelé. Le handler récupère le fichier et le transmet à WebKit via la fonction webkit_uri_scheme_request_finish.

En ayant ça, j'ai pu me lancer dans un debuggage long et terrifiant sur une énorme code base remplie d'asynchronisme ...

Pour ceux qui souhaitent compiler WebKit, prenez en compte qu'il vous faut au moins 2Go de RAM pour chaque cœurs CPU si vous compilez en parallèle (make -j ...).

L'un de mes soucis fut de ne pas tomber sur mes breakpoints. Cela peut arriver car WebKitGTK fait pas mal de multiprocessing donc forcément le debugger n'est pas pas capable de s'attacher au bon PID. J'ai dû le récuperer avec les différentes traces de debug. Puis, pour une raison que j'ignore, gdb était instable avec WebKit (fuites de mémoire, crashs ...), alors j'ai changé pour lldb qui a nettement mieux fonctionné.

En tous cas, après toutes ces galères, j'ai pu comprendre que GStreamer était utilisé pour la lecture des médias, et que le problème venait de là.

Pour ceux qui ne connaissent pas GStreamer, c'est une librairie permettant de faire du traitement de flux multimédia sous forme de pipeline, maintenue par le projet GNOME.

N'ayant jamais utilisé GStreamer, je me suis senti un peu perdu devant la complexité de cette librairie, je ne savais pas dans quelle direction aller. Je suis alors allé innocemment sur le dépôt GStreamer ouvrir une issue, mais elle fut aussitôt fermée :

GStreamer ticket comments

Je n'ai pas eu le temps de m'y pencher plus, alors j'ai mis ça de côté et j'ai continué ma vie.

Bond en avant, 2025

En finissant mes études, j'ai eu un moment de creux et je me suis dit que c'était sûrement le bon moment pour reprendre ces recherches, sachant que le problème existait toujours.

Avant de continuer, récapitulons:

  • Tauri utilise WebKitGTK sous Linux
  • Tauri implémente un protocole custom « asset » pour faciliter la lecture de fichiers présents sur le système de l'utilisateur
  • Lorsqu'on fournit un fichier audio/vidéo via un asset://, comme par exemple asset://path/to/file.mp4, il est transmis à GStreamer mais il ne comprend pas comment le gérer car il n'y a pas de handler.

  • Dans le ticket dont j'ai parlé précédemment, une personne avait fait référence à un test unitaire dans GStreamer qui enregistre justement un handler. Au premier abord je n'y compris pas grand chose, mais après avoir parcouru le code, j'ai pu apercevoir un passage ressemblant à :

    static gboolean
    gst_red_video_src_uri_set_uri (GstURIHandler * handler,
                                    const gchar * uri,
                                    GError ** error)
    {
      ...
    }
    
    static void
    gst_red_video_src_uri_handler_init (gpointer g_iface,
                                        gpointer iface_data)
    {
        ...
    }

    Et c'est exactement ce qu'il me fallait pour pouvoir créer un handler pour mon asset:// !.

    Il m'a fallu un peu de temps pour comprendre que dans mon cas, je devais écrire un plugin GStreamer. Quand j'ai pu debugger avec WebKit, j'étais arrivé à un moment donné au code d'erreur GST_CORE_ERROR_MISSING_PLUGIN. Et c'est en me souvenant de ça que j'ai compris que je devais écrire un plugin.

    Comment écrire un plugin GStreamer (non exhaustif)

    Une documentation spécialement pour ça existe. Et le test unitaire que j'ai pu lire, était un bon complément à ce que je voulais faire.

    L'idée principale du plugin est d'avoir un set_uri qui va récupérer le chemin du fichier, l'ouvrir et le transmettre dans un pipeline qui va traiter le flux pour avoir une sortie vidéo/audio.

    L'idée est donc d'avoir un plugin avec lequel on pourrait, via une commande, faire :

    gst-launch-1.0 uridecodebin uri=asset:///home/user/video.mp4
    ! autovideosink
    

    uridecodebin est un élément GStreamer qui permet de créer automatiquement une pipeline de décodage en fonction du type de média, en se basant sur l'uri fournie.

    autovideosink est un élément GStreamer qui permet d'afficher la vidéo dans une fenêtre.

    Pour réaliser cela, la pipeline interne du plugin est la suivante :

    GstTauriAsset

    Le premier élément filesrc va ouvrir le fichier puis externaliser la source via un GhostPad. Tout cela est réalisé dans la fonction set_uri.

    Si ta une meilleure idée, je serais ravi de la lire !

    Après avoir écrit et compilé le plugin, il faut que GStreamer puisse le trouver. La manière la plus simple est de définir la variable d'environnement GST_PLUGIN_PATH pointant vers le dossier contenant le plugin compilé. Avec ça, nous avons :


    Terry Davis contacte la CIA grâce à mon plugin

    Victoire !!!

    Contribution à Tauri

    Maintenant que nous avons quelque chose qui fonctionne, je peux enfin effectuer ma Pull Request sur Tauri. Lors de mes tests, j'ai écrit le plugin en C. Étant donné que Tauri est écrit en Rust, j'ai dû réécrire le plugin en Rust en utilisant la librairie gstreamer-rs. Pendant cette réécriture, mon expérience fut nettement plus agréable qu'en C, où pratiquement tout est fait avec des macros, grâce aux fonctionnalités de Rust (macros, interface, typage ...).

    La Pull Request est visible ici. Après discussion avec l'un des mainteneurs du projet sur Discord, cela ne devrais pas tarder à être merge.

    Merci d'avoir lu jusqu'ici !

    Pensée en passant

    Après avoir écrit le plugin, j'ai pensé que le problème aurait pu être réglé côté WebKit. Mais cela semble naturel d'écrire un plugin GStreamer pour pouvoir gérer spécialement ce cas-là où Tauri (ou autre librairie) souhaite avoir un comportement spécifique pour un protocol custom. Je trouve qu'il ne serait pas naturel de passer par l'API de WebKit pour enregistrer son handler ET ensuite écrire un plugin GStreamer pour gérer les médias pour cet handler WebKit précisément.

    Par ailleurs, bien que je ne l´ai mentionné nulle part dans le blog, le problème n'existe pas sur macOS/iOS et Windows. Pour Windows, cela peut s'expliquer car WebView2 est utilisé. Par contre, pour macOS/iOS, WebKit est utilisé. Cela laisse à penser qu'une solution pourrait sûrement être trouvée côté WebKitGTK en analysant la façon dont WebKit se comporte. Cela était dans mes pensées lorsque j'effectuais mes recherches, mais je n'avais pas creusé cette piste plus loin. Je reviendrai sur ces recherches lorsque j'aurai plus de temps et écrirai sûrement un autre blog sur ce sujet-là.

    Return home