Compilation de FFmpeg en WebAssembly (= ffmpeg.js): Partie 3 - Conversion d'avi en mp4



Liste des parties traduites de la série:


  1. Cuisine
  2. Compiler avec Emscripten
  3. Convertir avi en mp4 (vous êtes ici)



Dans cette partie, nous analyserons:



  1. Compilation de la bibliothèque FFmpeg avec des arguments optimisés.
  2. Gestion du système de fichiers Emscripten.
  3. Développement de ffmpeg.js v0.1.0 et conversion vidéo.



Compilation de la bibliothèque FFmpeg avec des arguments optimisés


Bien que l'objectif final de cette partie soit de créer ffmpeg.js v0.1.0 pour convertir avi en mp4, dans la partie précédente, nous n'avons créé qu'une version «nue» de FFmpeg, ce qui serait bien d'optimiser avec quelques paramètres.


  1. -Oz : optimisez le code et réduisez sa taille (de 30 à 15 Mo)
  2. -o javascript / ffmpeg-core.js : enregistrez les fichiers js et wasm dans le répertoire javascript. (d'où nous appellerons ffmpeg-core.js à partir de la bibliothèque d'encapsulation ffmpeg.js, qui fournit une belle API)
  3. -s MODULARIZE = 1 : créer une bibliothèque au lieu d'un utilitaire de ligne de commande (vous devrez modifier les sources, détails ci-dessous)
  4. -s EXPORTED_FUNCTIONS = "[_ ffmpeg]" : exporter la fonction C "ffmpeg" dans le monde JavaScript
  5. -s EXTRA_EXPORTED_RUNTIME_METHODS = "[cwrap, FS, getValue, setValue]" : fonctions supplémentaires pour travailler avec le système de fichiers et les pointeurs, des détails peuvent être trouvés dans l'article Interagir avec le code .
  6. -s ALLOW_MEMORY_GROWTH = 1 : supprimer la limite sur la mémoire consommée
  7. -lpthread : supprimé car nous prévoyons de créer notre propre travailleur. (il s'agit d'un carnet de commandes pour la quatrième partie des publications)

Plus de détails sur chacun des arguments peuvent être trouvés dans src / settings.js dans le référentiel github emscripten.


Lors de l'ajout de -s MODULARIZE = 1, nous devrons modifier le code source pour répondre aux exigences de modularité (en fait, se débarrasser de la fonction main ()). Vous ne devez modifier que trois lignes.


1. fftools / ffmpeg.c : renommer main en ffmpeg


- int main(int argc, char **argv) + int ffmpeg(int argc, char **argv) 

2. fftools / ffmpeg.h : ajoutez ffmpeg à la fin du fichier pour exporter la fonction


 + int ffmpeg(int argc, char** argv); #endif /* FFTOOLS_FFMPEG_H */ 

3. fftools / cmdutils.c : commentez exit (ret) afin que notre bibliothèque ne quitte pas le runtime pour nous (nous améliorerons ce point plus tard).


 void exit_program(int ret){ if (program_exit) program_exit(ret); - exit(ret); + // exit(ret); } 

Notre nouvelle version du script de compilation:


 emcc \ -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample \ -Qunused-arguments -Oz \ -o javascript/ffmpeg-core.js fftools/ffmpeg_opt.o fftools/ffmpeg_filter.o fftools/ffmpeg_hw.o fftools/cmdutils.o fftools/ffmpeg.o \ -lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lm \ -s MODULARIZE=1 \ -s EXPORTED_FUNCTIONS="[_ffmpeg]" \ -s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]" \ -s TOTAL_MEMORY=33554432 \ -s ALLOW_MEMORY_GROWTH=1 

ffmpeg-core.js est prêt!


Si vous avez de l'expérience avec ffmpeg, vous savez déjà à quoi ressemble une commande typique:


 $ ffmpeg -i input.avi output.mp4 

Et puisque nous utilisons la fonction ffmpeg au lieu de main, l'appel de commande ressemblera à ceci:


 const args = ['./ffmpeg', '-i', 'input.avi', 'output.mp4']; ffmpeg(args.length, args); 

Bien sûr, tout n'est pas si simple, nous devrons construire un pont entre les mondes JavaScript et C, alors commençons par le système de fichiers emscripten.


Gestion du système de fichiers Emscripten


Emscripten dispose d'un système de fichiers virtuel pour prendre en charge la lecture / écriture à partir de C, que ffmpeg-core.js utilise pour travailler avec des fichiers vidéo.


En savoir plus à ce sujet dans l' API du système de fichiers .


Pour que tout fonctionne, nous exportons l'API FS depuis emscripten, ce qui est dû au paramètre ci-dessus:


 -s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]" 

Pour enregistrer le fichier, vous devez préparer le tableau au format Uint8Array dans l'environnement de Node.js, ce qui peut être fait quelque chose comme ceci:


 const fs = require('fs'); const data = new Uint8Array(fs.readFileSync('./input.avi')); 

Et enregistrez-le dans le système de fichiers emscripten à l'aide de FS.writeFile ():


 require('./ffmpeg-core.js)() .then(Module => { Module.FS.writeFile('input.avi', data); }); 

Et pour télécharger le fichier depuis emscripten:


 require('./ffmpeg-core.js)() .then(Module => { const data = Module.FS.readFile('output.mp4'); }); 

Commençons à développer ffmpeg.js pour cacher ces complexités derrière une belle API.


Développement ffmpeg.js v0.1.0 et conversion vidéo


Le développement de ffmpeg.js n'est pas anodin, car vous devez constamment basculer entre les mondes de JavaScript et C, mais si vous êtes familier avec les pointeurs , il sera beaucoup plus facile de comprendre ce qui se passe ici.


Notre tâche consiste à développer ffmpeg.js comme ceci:


 const fs = require('fs'); const ffmpeg = require('@ffmpeg/ffmpeg'); (async () => { await ffmpeg.load(); const data = ffmpeg.transcode('./input.avi', 'mp4'); fs.writeFileSync('./output.mp4', data); })(); 

Tout d'abord, téléchargez ffmpeg-core.js, qui se fait traditionnellement de manière asynchrone afin de ne pas bloquer le thread principal.


Voici à quoi ça ressemble:


 const { setModule } = require('./util/module'); const FFmpegCore = require('./ffmpeg-core'); module.exports = () => ( new Promise((resolve, reject) => { FFmpegCore() .then((Module) => { setModule(Module); resolve(); }); }) ); 

Il peut sembler étrange que nous encapsulions une promesse dans une autre, car FFmpegCore () n'est pas une vraie promesse, mais juste une fonction qui simule l'API de promesse.


L'étape suivante consiste à utiliser Module pour obtenir la fonction ffmpeg à l'aide de la fonction cwrap :


 // int ffmpeg(int argc, char **argv) const ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']); 

Le premier argument de cwrap est le nom de la fonction (qui doit être dans EXPORTED_FUNCTIONS avec le soulignement précédent), le second est le type de la valeur de retour, le troisième est le type des arguments de la fonction (int argc et char ** argv).


Il est clair pourquoi argc est un nombre, mais pourquoi argv est-il également un nombre? argv est un pointeur et le pointeur stocke l'adresse en mémoire (de type 0xfffffff), le type de pointeur est donc non signé 32 bits dans WebAssembly. C'est pourquoi nous spécifions le nombre comme type argv .


Pour appeler ffmpeg (), le premier argument sera un nombre normal en JavaScript, mais le deuxième argument devrait être un pointeur vers un tableau de caractères (Uint8 en JavaScript).


Nous divisons cette tâche en 2 sous-tâches:


  1. Comment créer un pointeur sur un tableau de caractères?
  2. Comment créer un pointeur vers un tableau de pointeurs?

Nous allons résoudre le premier problème en créant l'utilitaire str2ptr :


 const { getModule } = require('./module'); module.exports = (s) => { const Module = getModule(); const ptr = Module._malloc((s.length+1)*Uint8Array.BYTES_PER_ELEMENT); for (let i = 0; i < s.length; i++) { Module.setValue(ptr+i, s.charCodeAt(i), 'i8'); } Module.setValue(ptr+s.length, 0, 'i8'); return ptr; }; 

Module._malloc () est similaire à malloc () en C, il alloue un morceau de mémoire sur le tas. Module.setValue () définit la valeur spécifique par pointeur.


N'oubliez pas d'ajouter 0 à la fin du tableau de caractères pour éviter les situations imprévues.


Après avoir traité la première sous-tâche, créez strList2ptr pour résoudre la seconde:


 const { getModule } = require('./module'); const str2ptr = require('./str2ptr'); module.exports = (strList) => { const Module = getModule(); const listPtr = Module._malloc(strList.length*Uint32Array.BYTES_PER_ELEMENT); strList.forEach((s, idx) => { const strPtr = str2ptr(s); Module.setValue(listPtr + (4*idx), strPtr, 'i32'); }); return listPtr; }; 

La principale chose à comprendre ici est que le pointeur est une valeur Uint32 dans JavaScript, donc listPtr est un pointeur vers un tableau Uint32 qui stocke des pointeurs vers un tableau Uint8.


En mettant tout cela ensemble, nous obtenons l'implémentation suivante de ffmepg.transcode () :


 const fs = require('fs'); const { getModule } = require('./util/module'); const strList2ptr = require('./util/strList2ptr'); module.exports = (inputPath, outputExt) => { const Module = getModule(); const data = new Uint8Array(fs.readFileSync(inputPath)); const ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']); const args = ['./ffmpeg', '-i', 'input.avi', `output.${outputExt}`]; Module.FS.writeFile('input.avi', data); ffmpeg(args.length, strList2ptr(args)); return Buffer.from(Module.FS.readFile(`output.${outputExt}`)); }; 

C'est fait! Nous avons maintenant ffmpeg.js v0.1.0 pour convertir avi en mp4.


Vous pouvez tester le résultat vous-même en installant la bibliothèque:


 $ npm install @ffmpeg/ffmpeg@0.1.0 

Et convertir le fichier comme ceci:


 const fs = require('fs'); const ffmpeg = require('@ffmpeg/ffmpeg'); (async () => { await ffmpeg.load(); const data = ffmpeg.transcode('./input.avi', 'mp4'); fs.writeFileSync('./output.mp4', data); })(); 

Gardez à l'esprit que jusqu'à présent, la bibliothèque ne fonctionne que pour Node.js, mais dans la partie suivante, nous ajouterons la prise en charge de Web-Worker (et child_process dans Node.js).


Codes sources:


Source: https://habr.com/ru/post/fr473166/


All Articles