tutorial // Dec 17, 2021

How to Convert Video Using FFmpeg in Node.js

How to build a command line interface in Node.js to convert videos using the FFmpeg command line tool.

How to Convert Video Using FFmpeg in Node.js

Getting started

For this tutorial, we're going to build a Node.js project from scratch. Make sure that you have the latest LTS version of Node.js installed on your machine. If you don't have Node.js installed, read this tutorial first before continuing.

If you have Node.js installed, next, we want to create a new folder for our project. This should be placed wherever you keep projects on your computer (e.g., ~/projects where ~ is the home folder or root on your computer).

Terminal

mkdir video-converter

Next, cd into that folder and run npm init -f:

Terminal

cd video-converter && npm init -f

This will automatically initialize a package.json file inside of your project folder. The -f stands for "force" and skips the automated wizard for generating this file (we skip it here for the sake of speed but feel free to omit the -f and follow the prompts).

Next, we're going to modify the package.json that was created to set the project type to be module:

Terminal

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

Doing this enables ESModules support in Node.js allowing us to use import and export in our code (as opposed to require() and modules.export.

Next, we need to install one dependency via NPM, inquirer:

Terminal

npm i inquirer

We'll use this package to create a command line prompt for gathering information about the video we're going to convert, the format we're going to output, and the location of the output file.

To complete our setup, the last thing we need to do is download a binary of the ffmpeg command line tool which will be the centerpiece of our work. This can be downloaded here (the version used for this tutorial is 4.2.1—make sure to select the binary for your operating system).

When you download this, it will be as a zip file. Unzip this and take the ffmpeg file (this is the binary script) and put it at the root of your project folder (e.g., ~/video-converter/ffmpeg).

That's all we need to get started building the video converter. Optionally, you can download a test video to convert here (make sure to place this in the root of the project folder for easy access).

Adding a command line prompt

To make our conversion script more user friendly, we're going to implement a command line prompt that asks the user questions and then collects and structures their input for easy use in our code. To get started, let's create a file called index.js inside of our project:

/index.js

import inquirer from 'inquirer'

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

First, we want to set up a boilerplate for our script. Because we'll be running our code in the command line via Node.js directly, here, instead of exporting a function, we're just writing our code directly in the file.

To guard against any errors we're using a try/catch block. This will allow us to write our code inside of the try part of the block and if it fails, "catch" any errors and redirect them to the catch block of the statement (where we're logging out the message of the error/exception).

Preemptively, up at the top of our file, we're importing the inquirer package we installed earlier. Next, we're going to use this to kick off our script and implement the questions we will ask a user before running FFmpeg to convert our 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);
}

Here, we're making use of the .prompt() method on the inquirer we imported from the inquirer package. To it, we pass an array of objects, each describing a question we want to ask our user. We have two types of questions for our users: input and list.

The input questions are questions where we want the user to type in (or paste) text in response while the list question asks the user to select from pre-defined list of options (like a multiple choice test question) that we control.

Here is what each option is doing:

  • type communicates the question type to Inquirer.
  • name defines the property on the answers object we get back from Inquirer where the answer to the question will be stored.
  • message defines the question text displayed to the user.
  • For the list type question, choices defines the list of choices the user will be able to select from to answer the question.

That's all we need to do to define our questions—Inquirer will take care of the rest from here. Once a user has completed all of the questions, we expect the inquirer.prompt() method to return a JavaScript Promise, so here, we chain on a call to .then() to say "after the questions are answered, call the function we're passing to .then()."

To that function, we expect inqurier.prompt() to pass us an object containing the answers the user gave us. To make these values easier to access and understand when we start to integrate FFmpeg, we break the answers object into individual variables, with each variable name being identical to the property name we expect on the answers object (remember, these will be the name property that we set on each of our question objects).

With this, before we move on to implementing FFmpeg, let's add a little bit of validation for our variables in case the user skips a question or leaves it blank.

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

Up at the top of the file, first, we've added fs (the built-in Node.js file system package). Back in the .then() callback for our call to inquirer.prompt(), we can see an if statement being defined just below our variables.

Here, the one variable we're concerned about is the fileToConvert. This is the original video file that we want to convert to one of our three different formats (mp4, mov, or mkv). To avoid breaking FFmpeg we need to verify two things: first, that the user has typed in a file path (or what we assume is a file path) and that a file actually exists at that path.

Here, that's exactly what we're verifying. First, does the fileToConvert variable contain a truthy value and second, if we pass the path that was input to fs.existsSync() can Node.js see a file at that location. If either of those return a falsey value, we want to return an error to the user and immediately exit our script. To do it, we call to the .exit() method on the Node.js process passing 0 as the exit code (this tells Node.js to exit without any output).

With this, we're ready to pull FFmpeg into play.

Wiring up FFmpeg

Recall that earlier when setting up our project, we downloaded what's known as a binary of FFmpeg and placed it at the root of our project as ffmpeg. A binary is a file which contains the entirety of a program in a single file (as opposed to a group of files linked together via imports like we may be used to when working with JavaScript and Node.js).

In order to run the code in that file, we need to call to it. In Node.js, we can do this by using the exec and execSync functions available on the child_process object exported from the child_process package (built into Node.js). Let's import child_process now and see how we're calling to FFmpeg (it's surprisingly 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);
}

Here, just beneath our if check to ensure our fileToConvert exists, we make a call to child_process.execSync() passing a string using backticks (this allows us to make use of JavaScript's string interpolation, or, embedding the values of variables into a string dynamically).

Inside of that string, we begin by writing ./ffmpeg. This is telling the execSync function to say "locate the file ffmpeg in the current directory and run it." Immediately after this, because we expect ffmpeg to exist, we start to pass the arguments (also known as "flags" when working with command line tools) to tell FFmpeg what we want to do.

In this case, we begin by saying that we want FFmpeg to convert an input file -i which is the fileToConvert we received from our user. Immediately after this—separated by a space—we pass the name of the output file with the format we want to convert our original file to as that file's extension (e.g., if we input homer-ice-cream.webm we might pass this output file as homer.mkv assuming we selected the "mkv" format in our prompt).

Because we're not 100% certain what inputs we'll get from the user, we make the output value we're passing to ffmpeg more resilient. To do it, we use a JavaScript ternary operator (a condensed if/else statement) to say "if the user gave us an outputName for the file, we want to concatenate that together with the outputPath and outputFormat as a a single string like ${outputPath}/${outputName}.${outputFormat}.

If they did not pass us an outputName, in the "else" portion of our ternary operator, we concatenate the outputPath with a hardcoded replacement for outputName "video" along with the outputFormat like ${outputPath}/video.${outputFormat}.

With all of this passed to child_process.execSync() before we consider our work complete, our last step is to pass an option to execSync() which is to tell the function how to handle the stdio or "standard input and output" from our call to ffmpeg. stdio is the name used to refer to the input, output, or errors logged out in a shell (the environment our code is running in when we use execSync).

Here, we need to pass the stdio option to execSync which takes an array of three strings, each string describing how to handle one of three of the types of stdio: stdin (standard input), stdout (standard output), stderr (standard error). For our needs, we don't want to do anything special for these and instead, prefer any output is logged directly to the terminal where we execute our Node script.

To do that, we need to pass an array that looks like ['inherit', 'inherit', 'inherit']. While we can certainly do that directly, frankly: it doesn't make any sense. So, to add context, here we take an object with key names equal to the type of stdio we want to configure the output setting for and values equal to the means for which we want to handle the output (in this case 'inherit' or "just hand the stdio to the parent running this code.").

Next, we pass that object to Object.values() to tell JavaScript to give us back an array containing only the values for each property in the object (the 'inherit' strings). In other words, we fulfill the expectations of execSync while also adding some context for us in the code so we don't get confused later.

That's it! As a final step, before we run our code, let's add an NPM script to our package.json file for quickly running our converter:

package.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"
  }
}

This one is a tiny addition. Here, we've added a new property "start" in the "scripts" object set to a string containing node index.js. This says "when we run npm start in our terminal, we want you to use Node.js to run the index.js file at the root of our project."

That's it! Let's give this all a test and see our converter in action:

Wrapping up

In this tutorial, we learned how to write a command line script using Node.js to run FFmpeg. As part of that process, we learned how to set up a prompt to collect data from a user and then hand that information off to FFmpeg when running it using the Node.js child_process.execSync() function.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode