Cómo convertir video usando FFmpeg en Node.js

Cómo construir una interfaz de línea de comandos en Node.js para convertir videos usando la herramienta de línea de comandos FFmpeg.

Primeros pasos

Para este tutorial, vamos a crear un proyecto de Node.js desde cero. Asegúrese de tener la última versión LTS de Node.js instalada en su máquina. Si no tiene instalado Node.js, lea primero este tutorial antes de continuar.

Si tiene instalado Node.js, a continuación, queremos crear una nueva carpeta para nuestro proyecto. Esto debe colocarse donde guarde los proyectos en su computadora (por ejemplo, ~/projects donde ~ es la carpeta de inicio o raíz en su computadora).

Terminal

mkdir video-converter

A continuación, cd en esa carpeta y ejecuta npm init -f :

Terminal

cd video-converter && npm init -f

Esto inicializará automáticamente un package.json archivo dentro de la carpeta de su proyecto. El -f significa "forzar" y omite el asistente automatizado para generar este archivo (lo omitimos aquí por razones de velocidad, pero siéntase libre de omitir el -f y sigue las indicaciones).

A continuación, vamos a modificar el package.json que fue creado para configurar el proyecto type ser module :

Terminal

{
  "name": "video-converter",
  "type": "module",
  "version": "1.0.0",
  ...
}

Hacer esto habilita la compatibilidad con ESModules en Node.js, lo que nos permite usar import y export en nuestro código (a diferencia de require() y modules.export .

A continuación, debemos instalar una dependencia a través de NPM, inquirer :

Terminal

npm i inquirer

Usaremos este paquete para crear un indicador de línea de comando para recopilar información sobre el video que vamos a convertir, el formato que vamos a generar y la ubicación del archivo de salida.

Para completar nuestra configuración, lo último que debemos hacer es descargar un binario del ffmpeg herramienta de línea de comandos que será la pieza central de nuestro trabajo. Esto se puede descargar aquí (la versión utilizada para este tutorial es 4.2.1; asegúrese de seleccionar el binario para su sistema operativo).

Cuando descargues esto, será como un archivo zip. Descomprima esto y tome el ffmpeg (este es el script binario) y colóquelo en la raíz de la carpeta de su proyecto (por ejemplo, ~/video-converter/ffmpeg ).

Eso es todo lo que necesitamos para comenzar a construir el convertidor de video. Opcionalmente, puede descargar un video de prueba para convertir aquí (asegúrese de colocarlo en la raíz de la carpeta del proyecto para facilitar el acceso).

Adición de una línea de comandos

Para hacer que nuestro script de conversión sea más fácil de usar, vamos a implementar una línea de comando que le hace preguntas al usuario y luego recopila y estructura su entrada para facilitar su uso en nuestro código. Para comenzar, creemos un archivo llamado index.js dentro de nuestro proyecto:

/index.js

import inquirer from 'inquirer'

try {
  // We'll write the code for our script here...
} catch (exception) {
  console.warn(exception.message);
}

Primero, queremos configurar un modelo para nuestro script. Debido a que ejecutaremos nuestro código en la línea de comandos a través de Node.js directamente, aquí, en lugar de exportar una función, simplemente escribiremos nuestro código directamente en el archivo.

Para protegernos contra cualquier error, estamos usando un try/catch bloquear. Esto nos permitirá escribir nuestro código dentro del try parte del bloque y, si falla, "captura" cualquier error y redirígelo al catch bloque de la declaración (donde estamos desconectando el message del error/exception ).

De manera preventiva, en la parte superior de nuestro archivo, estamos importando el inquirer paquete que instalamos anteriormente. A continuación, usaremos esto para iniciar nuestro script e implementar las preguntas que le haremos a un usuario antes de ejecutar FFmpeg para convertir nuestro video.

/index.js

import inquirer from 'inquirer';

try {
  inquirer.prompt([
    { type: 'input', name: 'fileToConvert', message: 'What is the path of the file you want to convert?' },
    {
      type: 'list',
      name: 'outputFormat',
      message: 'What format do you want to convert this to?',
      choices: [
        'mp4',
        'mov',
        'mkv',
      ],
    },
    { type: 'input', name: 'outputName', message: 'What should the name of the file be (without format)?' },
    { type: 'input', name: 'outputPath', message: 'Where do you want to store the converted file?' },
  ]).then((answers) => {
    const fileToConvert = answers?.fileToConvert;
    const outputPath = answers?.outputPath;
    const outputName = answers?.outputName;
    const outputFormat = answers?.outputFormat;

    // We'll call to FFmpeg here...
  });
} catch (exception) {
  console.warn(exception.message);
}

Aquí, estamos haciendo uso del .prompt() método en el inquirer importamos desde el inquirer paquete. A él, le pasamos una serie de objetos, cada uno de los cuales describe una pregunta que queremos hacerle a nuestro usuario. Tenemos dos tipos de preguntas para nuestros usuarios:input y list .

El input Las preguntas son preguntas en las que queremos que el usuario escriba (o pegue) texto en respuesta mientras el list pregunta le pide al usuario que seleccione de una lista predefinida de opciones (como una pregunta de prueba de opción múltiple) que controlamos.

Esto es lo que hace cada opción:

  • type comunica el tipo de pregunta al Inquirer.
  • name define la propiedad en el objeto de respuestas que recibimos de Inquirer donde se almacenará la respuesta a la pregunta.
  • message define el texto de la pregunta que se muestra al usuario.
  • Para el list escriba pregunta, choices define la lista de opciones que el usuario podrá seleccionar para responder la pregunta.

Eso es todo lo que tenemos que hacer para definir nuestras preguntas; Inquirer se encargará del resto desde aquí. Una vez que un usuario ha completado todas las preguntas, esperamos el inquirer.prompt() método para devolver una promesa de JavaScript, por lo que aquí, encadenamos una llamada a .then() para decir "después de que se respondan las preguntas, llame a la función que estamos pasando a .then() ."

A eso función, esperamos inqurier.prompt() para pasarnos un objeto que contenga el answers el usuario nos dio. Para facilitar el acceso y la comprensión de estos valores cuando comenzamos a integrar FFmpeg, rompemos el answers objeto en variables individuales, siendo cada nombre de variable idéntico al nombre de propiedad que esperamos en el answers objeto (recuerde, estos serán los name propiedad que establecemos en cada uno de nuestros objetos de pregunta).

Con esto, antes de continuar con la implementación de FFmpeg, agreguemos un poco de validación para nuestras variables en caso de que el usuario se salte una pregunta o la deje en blanco.

/index.js

import inquirer from 'inquirer';
import fs from 'fs';

try {
  inquirer.prompt([
    { type: 'input', name: 'fileToConvert', message: 'What is the path of the file you want to convert?' },
    {
      type: 'list',
      name: 'outputFormat',
      message: 'What format do you want to convert this to?',
      choices: [
        'mp4',
        'mov',
        'mkv',
      ],
    },
    { type: 'input', name: 'outputName', message: 'What should the name of the file be (without format)?' },
    { type: 'input', name: 'outputPath', message: 'Where do you want to store the converted file?' },
  ]).then((answers) => {
    const fileToConvert = answers?.fileToConvert;
    const outputPath = answers?.outputPath;
    const outputName = answers?.outputName;
    const outputFormat = answers?.outputFormat;

    if (!fileToConvert || (fileToConvert && !fs.existsSync(fileToConvert))) {
      console.warn('\nMust pass a video file to convert.\n');
      process.exit(0);
    }

    // We'll implement FFmpeg here...
  });
} catch (exception) {
  console.warn(exception.message);
}

En la parte superior del archivo, primero, agregamos fs (el paquete de sistema de archivos integrado de Node.js). De vuelta en el .then() devolución de llamada para nuestra llamada a inquirer.prompt() , podemos ver un if declaración que se define justo debajo de nuestras variables.

Aquí, la única variable que nos preocupa es fileToConvert . Este es el archivo de video original que queremos convertir a uno de nuestros tres formatos diferentes (mp4 , mov o mkv ). Para evitar romper FFmpeg, debemos verificar dos cosas:primero, que el usuario haya ingresado una ruta de archivo (o lo que suponemos es una ruta de archivo) y que un archivo realmente existe en ese camino.

Aquí, eso es exactamente lo que estamos verificando. Primero, ¿el fileToConvert la variable contiene un valor verdadero y segundo, si pasamos la ruta que se ingresó a fs.existsSync() Node.js puede ver un archivo en esa ubicación. Si alguno de ellos devuelve un valor falso, queremos devolver un error al usuario e inmediatamente salir de nuestro script. Para hacerlo llamamos al .exit() método en el proceso Node.js pasando 0 como el código de salida (esto le dice a Node.js que salga sin ningún resultado).

Con esto, estamos listos para poner en juego FFmpeg.

Cableado de FFmpeg

Recuerde que antes, al configurar nuestro proyecto, descargamos lo que se conoce como un binario de FFmpeg y lo colocó en la raíz de nuestro proyecto como ffmpeg . Un binario es un archivo que contiene la totalidad de un programa en un solo archivo (a diferencia de un grupo de archivos vinculados entre sí a través de importaciones, como estamos acostumbrados cuando trabajamos con JavaScript y Node.js).

Para ejecutar el código en ese archivo, debemos llamarlo. En Node.js, podemos hacer esto usando exec y execSync funciones disponibles en el child_process objeto exportado desde el child_process paquete (integrado en Node.js). Importemos child_process ahora y vea cómo estamos llamando a FFmpeg (es sorprendentemente simple):

/index.js

import child_process from 'child_process';
import inquirer from 'inquirer';
import fs from 'fs';

try {
  inquirer.prompt([ ... ]).then((answers) => {
    const fileToConvert = answers?.fileToConvert;
    const outputPath = answers?.outputPath;
    const outputName = answers?.outputName;
    const outputFormat = answers?.outputFormat;

    if (!fileToConvert || (fileToConvert && !fs.existsSync(fileToConvert))) {
      console.warn('\nMust pass a video file to convert.\n');
      process.exit(0);
    }

    child_process.execSync(`./ffmpeg -i ${fileToConvert} ${outputName ? `${outputPath}/${outputName}.${outputFormat}` : `${outputPath}/video.${outputFormat}`}`, {
      stdio: Object.values({
        stdin: 'inherit',
        stdout: 'inherit',
        stderr: 'inherit',
      })
    });
  });
} catch (exception) {
  console.warn(exception.message);
}

Aquí, justo debajo de nuestro if verifique para asegurarse de que nuestro fileToConvert existe, hacemos una llamada a child_process.execSync() pasar una cadena usando acentos graves (esto nos permite hacer uso de la interpolación de cadenas de JavaScript o incrustar los valores de las variables en una cadena dinámicamente).

Dentro de esa cadena, comenzamos escribiendo ./ffmpeg . Esto le dice al execSync función para decir "ubica el archivo ffmpeg en el directorio actual y ejecútelo". Inmediatamente después de esto, porque esperamos ffmpeg existir, comenzamos a pasar los argumentos (también conocidos como "banderas" cuando trabajamos con herramientas de línea de comandos) para decirle a FFmpeg lo que queremos hacer.

En este caso, comenzamos diciendo que queremos que FFmpeg convierta un archivo de entrada -i cual es el fileToConvert recibimos de nuestro usuario. Inmediatamente después de esto, separados por un espacio, pasamos el nombre del archivo de salida con el formato al que queremos convertir nuestro archivo original como la extensión de ese archivo (por ejemplo, si ingresamos homer-ice-cream.webm podríamos pasar este archivo de salida como homer.mkv asumiendo que seleccionamos el formato "mkv" en nuestro aviso).

Debido a que no estamos 100% seguros de qué entradas obtendremos del usuario, hacemos que el valor de salida que estamos pasando sea ffmpeg más resistente. Para hacerlo, usamos un operador ternario de JavaScript (una declaración if/else condensada) para decir "si el usuario nos dio un outputName para el archivo, queremos concatenar eso junto con el outputPath y outputFormat como una sola cadena como ${outputPath}/${outputName}.${outputFormat} .

Si lo hicieran no pásanos un outputName , en la parte "else" de nuestro operador ternario, concatenamos el outputPath con un reemplazo codificado para outputName "video" junto con el outputFormat como ${outputPath}/video.${outputFormat} .

Con todo esto pasado a child_process.execSync() antes de considerar nuestro trabajo completo, nuestro último paso es pasar una opción a execSync() que es decirle a la función cómo manejar el stdio o "entrada y salida estándar" de nuestra llamada a ffmpeg . stdio es el nombre que se usa para referirse a la entrada, la salida o los errores registrados en un shell (el entorno en el que se ejecuta nuestro código cuando usamos execSync ).

Aquí, necesitamos pasar el stdio opción a execSync que toma una matriz de tres cadenas, cada cadena describe cómo manejar uno de los tres tipos de stdio :stdin (entrada estándar), stdout (salida estándar), stderr (Error estándar). Para nuestras necesidades, no queremos hacer nada especial para estos y, en su lugar, preferimos que cualquier resultado se registre directamente en la terminal donde ejecutamos nuestro script de Nodo.

Para hacer eso, necesitamos pasar una matriz que se parece a ['inherit', 'inherit', 'inherit'] . Si bien ciertamente podemos hacerlo directamente, francamente:no tiene ningún sentido. Entonces, para agregar contexto, aquí tomamos un objeto con nombres clave iguales al tipo de stdio queremos configurar la configuración de salida para y valores iguales a los medios para los que queremos manejar la salida (en este caso 'inherit' o "simplemente entregue el stdio al padre que ejecuta este código").

A continuación, pasamos ese objeto a Object.values() para decirle a JavaScript que nos devuelva una matriz que contenga solo los valores de cada propiedad en el objeto (el 'inherit' instrumentos de cuerda). En otras palabras, cumplimos las expectativas de execSync al mismo tiempo que agrega algo de contexto para nosotros en el código para que no nos confundamos más tarde.

¡Eso es todo! Como paso final, antes de ejecutar nuestro código, agreguemos un script NPM a nuestro package.json archivo para ejecutar rápidamente nuestro convertidor:

paquete.json

{
  "name": "video-converter",
  "type": "module",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1",
    "convert": ""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "inquirer": "^8.2.0"
  }
}

Esta es una pequeña adición. Aquí, hemos agregado una nueva propiedad "start" en el "scripts" objeto establecido en una cadena que contiene node index.js . Esto dice "cuando ejecutamos npm start en nuestra terminal, queremos que use Node.js para ejecutar el index.js archivo en la raíz de nuestro proyecto."

¡Eso es todo! Probemos todo esto y veamos nuestro convertidor en acción:

Terminando

En este tutorial, aprendimos a escribir un script de línea de comandos usando Node.js para ejecutar FFmpeg. Como parte de ese proceso, aprendimos a configurar un aviso para recopilar datos de un usuario y luego pasar esa información a FFmpeg cuando se ejecuta usando Node.js child_process.execSync() función.