# Web Assembly & NodeJS

## Problématique et objectif

De la même manière que le navigateur a des performances limitées, car le JavaScript n’est pas conçu pour cela, les applications back-end Node.js subissent le même problème (même si ce n'est pas toujours visible).

Une application backend Node.js aura du mal à supporter des charges ou des traitements lourds.

Nous allons explorer les solutions disponibles pour résoudre ce problème, car certaines librairies NodeJS utilisent déjà le WebAssembly afin d'améliorer les performances.

En effet, Node.js est tout à fait en mesure de prendre en charge des modules `wasm` : “[https://nodejs.dev/en/learn/nodejs-with-webassembly/](https://nodejs.dev/en/learn/nodejs-with-webassembly/)”.

Nous allons créer un outil permettant d’encoder et de compresser une vidéo en utilisant ***FFmpeg*** côté serveur.

## FFmpeg.WASM

***FFmpeg*** est une collection de programmes conçus pour effectuer des opérations sur des flux audio ou vidéo (enregistrement, lecture ou conversion d'un format à un autre). Cette bibliothèque est largement employée par d'autres applications ou services tels que VLC, iTunes, etc.

***FFmpeg*** a été initialement écrit en C et C++, mais un portage WebAssembly existe sous la forme d'une librairie Node.js : “[https://github.com/ffmpegwasm/ffmpeg.wasm](https://github.com/ffmpegwasm/ffmpeg.wasm)”.

## Prérequis

Pour suivre ce TP, vous devez avoir :

- Un environnement de développement avec Node.js installé et à jour.
- Quelques connaissances dans la librairie Express.
- Un peu d'expérience en développement Web.
- Une vidéo de bonne qualité et pas trop longue : 
    - `mp4/mkv`
    - 1080p
    - Pas plus de 4 min.

## 1. Mise en place

Nous allons créer un premier projet pour la partie serveur.

Créez un dossier `ffmpeg-api` et exécutez la commande suivante à l'intérieur :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/oCPimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/oCPimage.png)

Il faut ensuite installer les librairies dont nous aurons besoin avec la commande suivante :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/SnNimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/SnNimage.png)

- `@ffmpeg/ffmpeg @ffmpeg/core` librairies servant à l'utilisation de **FFmpeg**
- `express` framework API léger
- `multer` Middleware permettant de prendre en charge les fichiers envoyés par formulaire sous le format `multipart/form-data`
- `p-queue` Gestionnaire de file d'attente avec support de concurrence

Nous allons installer la définition des types nécessaires pour `typescript`

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/Qpvimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/Qpvimage.png)

Pour automatiser le lancement de notre serveur, nous allons ajouter les scripts suivants dans le fichier `package.json` :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/NLvimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/NLvimage.png)

- `dev` : lance `nodemon` en lui demandant d'observer le dossier `src` pour les changements. À chaque changement, il relance le serveur avec `ts-node`.
- `build` : compile le projet avec [tsup](https://tsup.egoist.sh/#generate-declaration-file) et génère le fichier de destination.
- `start` : une fois le projet compilé, permet de lancer le serveur en production.
- `start:build` : compile en production, puis lance l’application directement.

L’argument `--experimental-wasm-threads` permet au WebAssembly de gérer le multi-threading. C’est une fonctionnalité encore expérimentale et qui peut être instable.

Il ne nous manque plus que le fichier `tsconfig.json` à la racine du projet qui configure la façon dont TypeScript fonctionne.

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/HQWimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/HQWimage.png)

- `baseUrl` : indique à TS que le dossier courant est l'entrée du projet.
- `target` : vers quelle version de JavaScript nous allons compiler.
- `module` : quels types de modules JavaScript nous allons utiliser.
- `paths` : définit les chemins personnalisés du projet.
- `ts-node` : demande à `ts-node` d'utiliser nos chemins personnalisés.

## 2. Préparation du serveur express

Nous allons mettre en place la base pour notre serveur express en créant le fichier `src/index.ts` avec le code suivant :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/lkLimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/lkLimage.png)

Nous allons essayer cela, en lançant la commande suivante et en nous rendant sur le lien [http://localhost:3000](http://localhost:3000) :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/Ktkimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/Ktkimage.png)

<table border="1" id="bkmrk--7" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><tbody><tr><td>[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/FK5image.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/FK5image.png)

</td><td>[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/OpSimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/OpSimage.png)

</td></tr></tbody></table>

Quand nous essayons d'atteindre notre page, nous obtenons le message `Cannot GET /`, ce qui signifie que notre serveur Express est bien en ligne. Il ne nous reste plus qu'à lui définir les routes dont nous aurons besoin prochainement.

## 3. Préparation du client web

Nous allons maintenant préparer la partie client, qui servira à envoyer la vidéo à convertir à notre serveur.

Dans un dossier à côté de celui de notre serveur `ffmpeg-api`, nous allons créer un nouveau dossier `ffmpeg-client`. Dans ce dossier, nous allons créer le fichier `index.html` avec le contenu suivant pour commencer :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/q3jimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/q3jimage.png)

Nous allons de ce pas créer le fichier `client.js` qui est importé par cette page, avec le contenu suivant :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/D6simage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/D6simage.png)

Avec ce petit code, allons vérifier que notre client réagit comme on le souhaite lorsqu’on sélectionne un fichier vidéo.

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/By5image.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/By5image.png)

Notre fichier est correctement chargé dans le navigateur, il nous reste simplement à l'envoyer au serveur et à récupérer le fichier vidéo retour.

Avant cela, nous allons faire quelques petits ajouts à ce fichier `client.js` pour y voir plus clair sur nos intentions.

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/khNimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/khNimage.png)

- `API_ENDPOINT` **:** est la route destinée à envoyer la vidéo au serveur pour la conversion.
- `submitButton.addEventListener` : vérifie désormais si le fichier sélectionné est bien une vidéo. Il s'occupera également de récupérer la réponse et d'afficher la vidéo convertie sur la page web.
- `function error*(message)` : petite fonction utilitaire pour afficher les erreurs sur la page web.
- `async function uploadVideo*(video)` : fonction servant à envoyer le fichier vidéo au serveur et à récupérer sa réponse. 
    - Les API web utilisent généralement le format `JSON` pour leurs requêtes. Le problème avec une vidéo, c'est qu'il faut l'encoder en `base64` avant de pouvoir encapsuler ses données dans un `JSON`... Cela augmenterait alors la taille de la vidéo (déjà assez lourde) d'au moins 30 % !
    - Pour remédier à cela, nous allons utiliser le format [multipart requests](https://pspdfkit.com/blog/2021/a-brief-tour-of-multipart-requests/) à la place.
    - Ce format permet d'envoyer des fichiers binaires tels quels, sans avoir à les encoder au préalable.

## 4. Récupération de la vidéo côté serveur

Maintenant que tout est en place côté client, il faut que notre serveur Express puisse recevoir la vidéo, pour ensuite la convertir en utilisant la librairie FFmpeg en WebAssembly (qui est le but principal).

Il faut également s'assurer que le fichier reçu est bien un fichier vidéo, d'une taille raisonnable (comme le fichier sera stocké en mémoire, il vaut mieux qu'il ne dépasse pas un certain poids pour ne pas mettre le serveur à mal).

Nous allons utiliser le middleware "multer" évoqué plus tôt qui va nous permettre de définir une règle pour les fichiers envoyés via le format [multipart requests](https://pspdfkit.com/blog/2021/a-brief-tour-of-multipart-requests/).

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/cnlimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/cnlimage.png)

Nous avons maintenant tout ce qu’il faut pour créer notre route `/convert` :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/6aCimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/6aCimage.png)

- `uploadMiddleware.single('video')` va mettre en place le middleware défini plus haut et bloquer la requête si le fichier ne respecte pas le format attendu.
- Le contenu du fichier sera dans `req.file.buffer`.

## 5. Conversion de la vidéo avec FFmpeg

Nous allons enfin pouvoir utiliser notre remarquable librairie en WebAssembly ! Elle était, à l'origine, pensée pour être exécutée côté navigateur, mais avec les versions les plus récentes de Node.js, c'est désormais possible sans problème.

Dans un premier temps, nous mettrons en place l'instance de **FFmpeg** afin de pouvoir l'utiliser au moment opportun :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/r01image.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/r01image.png)

- \*`async function* getFFmpeg()` renvoie l’instance de l’outil **FFmpeg**, une fois que ce dernier aura été chargé (cela peut prendre plusieurs secondes au démarrage)

Nous pouvons maintenant appeler cette fonction dans notre route `convert` :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/HjTimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/HjTimage.png)

- Nous récupérons bien une instance de **FFmpeg**
- Nous définissons aussi quelques variables, dont une pour accueillir les données de la vidéo une fois cette dernière convertie

Il faut ensuite donner à l’instance **FFmpeg** notre vidéo d’entrée pour que ce dernier puisse la traiter

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/TaCimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/TaCimage.png)

Il ne nous reste plus qu’à lancer la commande **FFmpeg** avec les bons arguments afin de convertir notre vidéo ! Nous pouvons retrouver tous ces arguments sur la documentation officielle “[https://trac.ffmpeg.org/wiki/Encode/H.264](https://trac.ffmpeg.org/wiki/Encode/H.264)”

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/Kxvimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/Kxvimage.png)

- `i` : Flux d'entrée
- `vcodec` : Codec à utiliser (x264, x265, VP8, VP9, WAV, etc.)
- `crf` : **Facteur de taux constant**, contrôle le débit de sortie, qui varie entre 0 et 51. Plus la valeur est élevée, plus la vidéo sera légère, mais de moins bonne qualité et vice versa. La valeur par défaut est 23.
- `preset` : Préréglage qui fournira une certaine vitesse d'encodage. Un préréglage plus lent fournira une meilleure compression de la vidéo, mais à un temps d'encodage beaucoup plus long. Cette option doit être ajustée avec le `crf` afin de trouver le bon équilibre poids/qualité/temps d'encodage.
- `c:a` : Encodage de l'audio (MP3, AAC, etc.)
- `b:a` : Débit de sortie du flux audio
- Le dernier argument sera le nom du fichier de sortie.

Il faut alors attendre que l'encodage de la vidéo se termine... Selon le poids et la durée de la vidéo, cela peut prendre plus ou moins de temps.

Pour récupérer notre vidéo de sortie, nous allons à nouveau utiliser `ffmpeg.FS` pour lire les données du système de fichiers.

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/8yAimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/8yAimage.png)

Nous pouvons ensuite supprimer notre fichier d’entrée et de sortie du contexte **FFmpeg** pour libérer de la mémoire.

Avec un peu de refacto, notre fichier final sera le suivant :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/raCimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/raCimage.png)

Voilà un exemple de conversion avec une célèbre vidéo :

<table border="1" id="bkmrk--20" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><tbody><tr><td>[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/BSmimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/BSmimage.png)

</td><td>[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/kzhimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/kzhimage.png)

</td></tr></tbody></table>

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/pbcimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/pbcimage.png)

## 6. Bonus : Gérer plusieurs demandes de conversion

Il n'est pas encore possible d'utiliser WebAssembly et Node.js pour traiter deux requêtes en parallèle (du moins, pas avec la librairie `ffmpeg.wasm`). La bibliothèque de threads WebAssembly a été introduite dans V8/Chrome depuis 2019. Cependant, cela reste encore expérimental dans Node.js et il faut activer le flag `--experimental-wasm-threads` pour l'utiliser.

Même si vous essayez (avec le flag) de lancer deux conversions simultanément, vous obtiendrez la sortie suivante :

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/29eimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/29eimage.png)

Ne pouvant donc pas gérer plusieurs demandes en même temps, il est nécessaire de mettre en place un système de file d'attente afin d'éviter de faire planter notre serveur.

Pour cela, nous allons importer la librairie [p-queue](https://www.npmjs.com/package/p-queue) qui permet de créer des files d'attente concurrentielles.

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/Zlmimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/Zlmimage.png)

Cela nous permet de créer une file d'attente concurrentielle de maximum un traitement à la fois.

Il suffit alors d'utiliser cette file au moment où nous devons encoder.

[![image.png](https://wiki.nospy.fr/uploads/images/gallery/2023-05/scaled-1680-/eNHimage.png)](https://wiki.nospy.fr/uploads/images/gallery/2023-05/eNHimage.png)

## Récapitulatif

<video controls="controls" height="400" width="800"><source src="https://wiki.nospy.fr/attachments/14"></source></video>

## Ressources

[wasm-ffmpeg.zip](https://wiki.nospy.fr/attachments/15)

[https://www.secondstate.io/articles/getting-started-with-rust-function/](https://www.secondstate.io/articles/getting-started-with-rust-function/)

[https://ffmpegwasm.netlify.app/](https://ffmpegwasm.netlify.app/)

[https://nodejs.dev/en/learn/nodejs-with-webassembly/](https://nodejs.dev/en/learn/nodejs-with-webassembly/)

[https://www.digitalocean.com/community/tutorials/how-to-build-a-media-processing-api-in-node-js-with-express-and-ffmpeg-wasm#prerequisites](https://www.digitalocean.com/community/tutorials/how-to-build-a-media-processing-api-in-node-js-with-express-and-ffmpeg-wasm#prerequisites)

[https://trac.ffmpeg.org/wiki/Encode/H.264](https://trac.ffmpeg.org/wiki/Encode/H.264)

[https://www.ffmpeg.org/ffmpeg-filters.html](https://www.ffmpeg.org/ffmpeg-filters.html)

[https://ffmpeg.org/ffmpeg.html](https://ffmpeg.org/ffmpeg.html)

[https://ffmpeg.org/documentation.html](https://ffmpeg.org/documentation.html)

[https://jeromewu.github.io/ffmpeg-wasm-a-pure-webassembly-javascript-port-of-ffmpeg/](https://jeromewu.github.io/ffmpeg-wasm-a-pure-webassembly-javascript-port-of-ffmpeg/)