Kompilieren von FFmpeg zu WebAssembly (= ffmpeg.js): Teil 3 - Konvertieren von AVI in MP4



Liste der übersetzten Teile der Serie:


  1. Kochen
  2. Kompilieren mit Emscripten
  3. Konvertiere avi in ​​mp4 (du bist hier)



In diesem Teil werden wir analysieren:



  1. Kompilierung der FFmpeg-Bibliothek mit optimierten Argumenten.
  2. Emscripten Dateisystemverwaltung.
  3. Entwicklung von ffmpeg.js v0.1.0 und Videokonvertierung.



Kompilieren der FFmpeg-Bibliothek mit optimierten Argumenten


Obwohl das endgültige Ziel dieses Teils darin besteht, ffmpeg.js v0.1.0 für die Konvertierung von avi in ​​mp4 zu erstellen, haben wir im vorherigen Teil nur eine "nackte" Version von FFmpeg erstellt, die sich mit mehreren Parametern optimieren lässt.


  1. -Oz : Optimieren Sie den Code und reduzieren Sie seine Größe (von 30 auf 15 MB)
  2. -o javascript / ffmpeg-core.js : speichere js- und wasm-Dateien im Javascript-Verzeichnis. (von wo aus wir ffmpeg-core.js aus der Wrapper-Bibliothek ffmpeg.js aufrufen, die eine schöne API bietet)
  3. -s MODULARIZE = 1 : Erstellen Sie eine Bibliothek anstelle eines Befehlszeilenprogramms (Sie müssen die Quellen ändern, Details unten).
  4. -s EXPORTED_FUNCTIONS = "[_ ffmpeg]" : Exportiert die C-Funktion "ffmpeg" in die JavaScript-Welt
  5. -s EXTRA_EXPORTED_RUNTIME_METHODS = "[cwrap, FS, getValue, setValue]" : Weitere Funktionen für die Arbeit mit dem Dateisystem und Zeigern finden Sie im Artikel Interaktion mit Code .
  6. -s ALLOW_MEMORY_GROWTH = 1 : Entfernen Sie das Limit für den verbrauchten Speicher
  7. -lpthread : entfernt, da wir planen, einen eigenen Worker zu erstellen. (Dies ist ein Rückstand für den vierten Teil der Veröffentlichungen)

Weitere Details zu den einzelnen Argumenten finden Sie in src / settings.js im emscripten des Github-Repositorys.


Wenn Sie -s MODULARIZE = 1 hinzufügen , müssen Sie den Quellcode ändern, um die Anforderungen der Modularität zu erfüllen ( entfernen Sie tatsächlich die Funktion main ()). Sie müssen nur drei Zeilen ändern.


1. fftools / ffmpeg.c : benenne main in ffmpeg um


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

2. fftools / ffmpeg.h : Fügen Sie am Ende der Datei ffmpeg hinzu, um die Funktion zu exportieren


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

3. fftools / cmdutils.c : kommentiere exit (ret) aus, damit unsere Bibliothek die Laufzeit für uns nicht beendet (wir werden diesen Punkt später verbessern).


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

Unsere neue Version des Kompilierungsskripts:


 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 ist fertig!


Wenn Sie Erfahrung mit ffmpeg haben, wissen Sie bereits, wie ein typischer Befehl aussieht:


 $ ffmpeg -i input.avi output.mp4 

Und da wir die Funktion ffmpeg anstelle von main verwenden, sieht der Befehlsaufruf folgendermaßen aus:


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

Natürlich ist nicht alles so einfach, wir müssen eine Brücke zwischen den Welten von JavaScript und C schlagen. Beginnen wir also mit dem emscripten-Dateisystem.


Emscripten Dateisystemverwaltung


Emscripten verfügt über ein virtuelles Dateisystem zur Unterstützung des Lesens / Schreibens aus C, das ffmpeg-core.js für die Arbeit mit Videodateien verwendet.


Weitere Informationen hierzu finden Sie in der Dateisystem-API .


Damit alles funktioniert, exportieren wir die FS-API aus emscripten, was auf den obigen Parameter zurückzuführen ist:


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

Um die Datei zu speichern, müssen Sie das Array im Uint8Array-Format in der Umgebung von Node.js vorbereiten. Dies kann folgendermaßen erfolgen:


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

Und speichern Sie es mit FS.writeFile () im emscripten-Dateisystem:


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

Und um die Datei von emscripten herunterzuladen:


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

Beginnen wir mit der Entwicklung von ffmpeg.js, um diese Komplexität hinter einer schönen API zu verbergen.


Entwicklung ffmpeg.js v0.1.0 und Videokonvertierung


Die Entwicklung von ffmpeg.js ist nicht trivial, da Sie ständig zwischen den Welten von JavaScript und C wechseln müssen. Wenn Sie jedoch mit Zeigern vertraut sind, ist es viel einfacher zu verstehen, was hier passiert.


Unsere Aufgabe ist es, ffmpeg.js wie folgt zu entwickeln:


 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); })(); 

Laden Sie zunächst ffmpeg-core.js herunter, was traditionell asynchron erfolgt, um den Hauptthread nicht zu blockieren.


So sieht es aus:


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

Es mag seltsam erscheinen, dass wir ein Versprechen in ein anderes verpacken. Dies liegt daran, dass FFmpegCore () kein echtes Versprechen ist, sondern nur eine Funktion, die die Versprechen-API simuliert.


Der nächste Schritt besteht darin, Module zu verwenden, um die Funktion ffmpeg mithilfe der Funktion cwrap abzurufen :


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

Das erste Argument für cwrap ist der Name der Funktion (der in EXPORTED_FUNCTIONS mit dem vorherigen Unterstrich enthalten sein muss), das zweite ist der Typ des Rückgabewerts, das dritte ist der Typ der Argumente der Funktion (int argc und char ** argv).


Es ist klar, warum argc eine Zahl ist, aber warum ist argv auch eine Zahl? argv ist ein Zeiger, und der Zeiger speichert die Adresse im Speicher (vom Typ 0xfffffff), sodass der Zeigertyp in WebAssembly 32-Bit ohne Vorzeichen ist. Deshalb geben wir die Nummer als Argv- Typ an.


Um ffmpeg () aufzurufen, ist das erste Argument eine reguläre Zahl in JavaScript, das zweite Argument sollte jedoch ein Zeiger auf ein Array von Zeichen sein (Uint8 in JavaScript).


Wir teilen diese Aufgabe in 2 Unteraufgaben:


  1. Wie erstelle ich einen Zeiger auf ein Array von Zeichen?
  2. Wie erstelle ich einen Zeiger auf ein Array von Zeigern?

Wir werden das erste Problem lösen, indem wir das Dienstprogramm str2ptr erstellen :


 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 () ähnelt malloc () in C, weist dem Heap einen Teil des Speichers zu. Module.setValue () setzt den spezifischen Wert per Zeiger.


Denken Sie daran, am Ende des Zeichenarrays 0 hinzuzufügen, um unvorhergesehene Situationen zu vermeiden.


Nachdem Sie sich mit der ersten Unteraufgabe befasst haben, erstellen Sie strList2ptr , um die zweite zu lösen:


 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; }; 

Hier ist vor allem zu verstehen, dass der Zeiger ein Uint32-Wert in JavaScript ist. ListPtr ist also ein Zeiger auf ein Uint32-Array, in dem Zeiger auf ein Uint8-Array gespeichert sind.


Wenn wir alles zusammenfügen, erhalten wir die folgende Implementierung von 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}`)); }; 

Fertig Jetzt haben wir ffmpeg.js v0.1.0 zum Konvertieren von avi in ​​mp4.


Sie können das Ergebnis selbst testen, indem Sie die Bibliothek installieren:


 $ npm install @ffmpeg/ffmpeg@0.1.0 

Und konvertieren Sie die Datei wie folgt:


 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); })(); 

Denken Sie daran, dass die Bibliothek bisher nur für Node.js funktioniert. Im nächsten Teil werden wir jedoch die Unterstützung für Web-Worker (und child_process in Node.js) hinzufügen.


Quellcodes:


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


All Articles