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.
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.