Lista de partes traducidas de la serie:
- Cocinando
- Compilando con Emscripten
- Convierte avi a mp4 (estás aquí)
En esta parte, analizaremos:
- Compilación de la biblioteca FFmpeg con argumentos optimizados.
- Gestión del sistema de archivos Emscripten.
- Desarrollo de ffmpeg.js v0.1.0 y conversión de video.
Compilar la biblioteca FFmpeg con argumentos optimizados
Aunque el objetivo final de esta parte es crear ffmpeg.js v0.1.0 para convertir avi a mp4, en la parte anterior creamos solo una versión "básica" de FFmpeg, que sería bueno optimizar con varios parámetros.
- -Oz : optimiza el código y reduce su tamaño (de 30 a 15 MB)
- -o javascript / ffmpeg-core.js : guarde los archivos js y wasm en el directorio javascript. (desde donde llamaremos ffmpeg-core.js desde la biblioteca de envoltura ffmpeg.js, que proporciona una buena API)
- -s MODULARIZE = 1 : crea una biblioteca en lugar de una utilidad de línea de comandos (deberás modificar las fuentes, detalles a continuación)
- -s EXPORTED_FUNCTIONS = "[_ ffmpeg]" : exporta la función ffmpeg C al mundo JavaScript
- -s EXTRA_EXPORTED_RUNTIME_METHODS = "[cwrap, FS, getValue, setValue]" : funciones adicionales para trabajar con el sistema de archivos y punteros, los detalles se pueden encontrar en el artículo Interactuar con el código .
- -s ALLOW_MEMORY_GROWTH = 1 : elimina el límite en la memoria consumida
- -lpthread : eliminado porque planeamos crear nuestro propio trabajador. (Esta es una cartera de pedidos para la cuarta parte de las publicaciones)
Se pueden encontrar más detalles sobre cada uno de los argumentos en src / settings.js en el repositorio de github emscripten.
Al agregar -s MODULARIZE = 1, necesitaremos modificar el código fuente para cumplir con los requisitos de modularidad (de hecho, deshacerse de la función main ()). Tienes que cambiar solo tres líneas.
1. fftools / ffmpeg.c : renombra main a ffmpeg
- int main(int argc, char **argv) + int ffmpeg(int argc, char **argv)
2. fftools / ffmpeg.h : agregue ffmpeg al final del archivo para exportar la función
+ int ffmpeg(int argc, char** argv); #endif /* FFTOOLS_FFMPEG_H */
3. fftools / cmdutils.c : comenta la salida (ret) para que nuestra biblioteca no salga del tiempo de ejecución (mejoraremos este punto más adelante).
void exit_program(int ret){ if (program_exit) program_exit(ret); - exit(ret); + // exit(ret); }
Nuestra nueva versión del script de compilación:
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á listo!
Si tiene experiencia con ffmpeg, ya sabe cómo se ve un comando típico:
$ ffmpeg -i input.avi output.mp4
Y como usamos la función ffmpeg en lugar de main, la llamada de comando se verá así:
const args = ['./ffmpeg', '-i', 'input.avi', 'output.mp4']; ffmpeg(args.length, args);
Por supuesto, no todo es tan simple, tendremos que construir un puente entre los mundos de JavaScript y C, así que comencemos con el sistema de archivos emscripten.
Gestión del sistema de archivos emscripten
Emscripten tiene un sistema de archivos virtual para admitir la lectura / escritura desde C, que ffmpeg-core.js utiliza para trabajar con archivos de video.
Lea más sobre esto en la API del sistema de archivos .
Para que todo funcione, exportamos la API de FS desde emscripten, que se debe al parámetro anterior:
-s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]"
Para guardar el archivo, debe preparar la matriz en el formato Uint8Array en el entorno de Node.js, que se puede hacer de la siguiente manera:
const fs = require('fs'); const data = new Uint8Array(fs.readFileSync('./input.avi'));
Y guárdelo en el sistema de archivos emscripten usando FS.writeFile ():
require('./ffmpeg-core.js)() .then(Module => { Module.FS.writeFile('input.avi', data); });
Y para descargar el archivo de emscripten:
require('./ffmpeg-core.js)() .then(Module => { const data = Module.FS.readFile('output.mp4'); });
Comencemos a desarrollar ffmpeg.js para ocultar estas complejidades detrás de una hermosa API.
Desarrollo ffmpeg.js v0.1.0 y conversión de video
El desarrollo de ffmpeg.js no es trivial, ya que constantemente necesita cambiar entre los mundos de JavaScript y C, pero si está familiarizado con los punteros , será mucho más fácil entender lo que está sucediendo aquí.
Nuestra tarea es desarrollar ffmpeg.js como este:
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); })();
Primero, descargue ffmpeg-core.js, que tradicionalmente se realiza de forma asíncrona para no bloquear el hilo principal.
Así es como se ve:
const { setModule } = require('./util/module'); const FFmpegCore = require('./ffmpeg-core'); module.exports = () => ( new Promise((resolve, reject) => { FFmpegCore() .then((Module) => { setModule(Module); resolve(); }); }) );
Puede parecer extraño que envuelvamos una promesa en otra, esto se debe a que FFmpegCore () no es una promesa real, sino solo una función que simula la API de promesa.
El siguiente paso es usar el Módulo para obtener la función ffmpeg usando la función cwrap :
// int ffmpeg(int argc, char **argv) const ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']);
El primer argumento para cwrap es el nombre de la función (que debe estar en EXPORTED_FUNCTIONS con el guión bajo anterior), el segundo es el tipo del valor de retorno, el tercero es el tipo de argumentos de la función (int argc y char ** argv).
Está claro por qué argc es un número, pero ¿por qué argv también es un número? argv es un puntero, y el puntero almacena la dirección en la memoria (de tipo 0xfffffff), por lo que el tipo de puntero no tiene signo de 32 bits en WebAssembly. Es por eso que especificamos el número como el tipo argv .
Para llamar a ffmpeg (), el primer argumento será un número regular en JavaScript, pero el segundo argumento debe ser un puntero a una matriz de caracteres (Uint8 en JavaScript).
Dividimos esta tarea en 2 subtareas:
- ¿Cómo crear un puntero a una serie de caracteres?
- ¿Cómo crear un puntero a una matriz de punteros?
Resolveremos el primer problema creando la utilidad 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 () es similar a malloc () en C, asigna una porción de memoria en el montón. Module.setValue () establece el valor específico por puntero.
Recuerde agregar 0 al final de la matriz de caracteres para evitar situaciones imprevistas.
Después de ocuparse de la primera subtarea, cree strList2ptr para resolver la segunda:
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; };
Lo principal que hay que entender aquí es que el puntero es un valor Uint32 dentro de JavaScript, por lo que listPtr es un puntero a una matriz Uint32 que almacena punteros en una matriz Uint8.
Al poner todo junto, obtenemos la siguiente implementación 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}`)); };
Hecho Ahora tenemos ffmpeg.js v0.1.0 para convertir avi a mp4.
Puede probar el resultado usted mismo instalando la biblioteca:
$ npm install @ffmpeg/ffmpeg@0.1.0
Y convirtiendo el archivo así:
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); })();
Solo tenga en cuenta que hasta ahora la biblioteca solo funciona para Node.js, pero en la siguiente parte agregaremos soporte para web-worker (y child_process en Node.js).
Códigos fuente: