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ù je voulais lire un fichier audio. Le plus simple est de lire le fichier depuis le frontend. Tauri prédéfinit 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 si une issue GitHub existait, et ...
oh boy ...

Ce problème semble existe depuis 3 ans, et beaucoup de personnes semblent l'avoir rencontré, donc on s'attaque à du lourd.
Quand un problème se présente, généralement les personnes attendent 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 contourner le problème. 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 au frontend de manière efficace...
L'une des solutions possibles est de lire le fichier en brut et de 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 possède un bugzilla et après avoir fait une recherche, je suis tombé sur un post datant de 2015 (10 ans à compter d'aujourd'hui !) 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 du debuggage long et terrifiant sur une énorme code base remplis d'asynchronisme ...
Pour ceux qui souhaitent compiler WebKit, prenez en compte qu'il vous faut au moins 2Go de RAM pour chaque coeur CPU si vous compilez en parallèle (make -j ...).
L'un de mes soucis étais le fait de ne pas tomber sur mes breakpoints. Ce qui s'explique par le fait que WebKitGTK fait pas mal de multiprocessing donc forcément le debugger n'est pas pas capable de s'attacher au bon PID. J'ai du le récuperer avec les différentes traces de debug. Puis je ne connais pas la raison, mais dans mon cas gdb était instable avec WebKit (fuite de mémoire, crash), alors j'ai changé pour lldb qui fonctionne nettement mieux.
En tous cas après ses 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 quel direction aller. Je suis alors allé de manière innocente sur le dépôt GStreamer ouvrir une issue qui fut aussitôt fermée :

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 de reprendre ces recherches en sachant que le problème existait toujours.
Avant de continuer, récapitulons:
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é avant, une personne m'avait fait référence à un test unitaire dans GStreamer qui justement enregistre un handler. À premier abord je ne comprenais pas grand chose, après avoir parcouru le code, j'ai pu apercevoir un bout de code 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 auquel on pourrait, via une commande, faire :
gst-launch-1.0 uridecodebin uri=asset:///home/user/video.mp4
! autovideosink
uridecodebinest un élément Gstreamer qui permet d'automatiquement créer une pipeline de décodage en fonction du type de média basé sur l'uri fournie.
autovideosinkest 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 :

Nous avons notre premier élément filesrc qui 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 da 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é, et avec ça, nous avons :
Terry Davis contacte la CIA grâce à mon plugin
Victoire !!!
Contribution à Tauri
Maintenant que nous avons quelque chose qui marche, je peux enfin effectuer ma Pull Request sur Tauri. Lors de mes tests j'ai écrit le plugin en C, et é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, l'expérience a nettement été plus agréable qu'en C, dû aux fonctionnalités de Rust (macros, interface, typage) comparé au C, où tout est pratiquement fait avec des macros.
La Pull Request est visible ici. Après discussion l'un des mainteneurs du projet sur Discord, ça 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 spécialement gérer ce cas-là ou Tauri (ou autre librairie) souhaite avoir un comportement spécifique pour un protocol custom. Mais je trouve que ce n'est 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, je ne l´ai mentionné nulle part dans le blog, mais 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é. Ce qui laisse à penser qu'une solution pourrait sûrement être trouvée côté WebKitGTK en analysant comme WebKit ce comporte. Cela était dans mes pensées lorsque j'effectuais mes recherches, mais je n'avais pas creusé plus loin cette piste. Je reviendrai sur ces recherches lorsque j'aurai plus de temps et écrirai sûrement un autre blog sur ce sujet-là.