Open-source project
75lb/handbrake-js avatar
75lb/handbrake-js

handbrake-js drives HandBrakeCLI from Node with three call styles

Video encoding / transcoding / converting for node.js

589 stars68 forksJavaScriptNOASSERTION

At a glance

What is it?
handbrake-js is a Node.js wrapper around the HandBrake CLI that spawns the binary, parses its output into events and a promise, and can also install itself as a handbrake command. It suits services that transcode video without shelling out by hand.
Who is it for?
Adopt handbrake-js if your Node service needs to transcode video and you would rather drive the HandBrake CLI through events and a promise than parse command output yourself. Do not adopt it if you need fine control over the encoder, because the options object is passed straight to HandBrakeCLI, and do not assume a Linux container comes with the binary.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 167 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Three ways to call the same binary

The library is a wrapper around HandBrake, the tool for converting video from nearly any format to a selection of modern, widely supported codecs, and it can process most common multimedia files along with DVD and BluRay sources that contain no copy protection. The API offers three exported functions and the difference between them is about how you observe the work. spawn returns a Handbrake instance you listen to for events, exec runs the process and calls a callback when it finishes, and run is identical to exec except that it returns a promise. The package exports both JavaScript and CommonJS modules, and the supported runtimes are Node.js 14 or newer. The options object you pass is not a bespoke configuration, it is the HandBrakeCLI options passed directly, which is the single most important design decision in the library. You are not learning a second configuration language, you are learning HandBrake's, and the documentation links straight to the CLI guide for each option.

spawn gives you progress events on a child process

The event style is the interesting one, because transcoding is a long job and you usually want to report on it. The spawn function is exported, synchronous, and returns a Handbrake instance on which you can listen for progress events. The inner Handbrake object exposes output, options and an eError enum, a cancel method, and the events start, begin, progress with a progress payload, output with an output payload, error with an error payload, end, complete and cancelled. The library usage example is short:

js
const hbjs = require('handbrake-js')

hbjs.spawn({ input: 'something.avi', output: 'something.m4v' })
  .on('error', err => {
    // invalid user input, no video found etc
  })
  .on('progress', progress => {
    console.log(
      'Percent complete: %s, ETA: %s',
      progress.percentComplete,
      progress.eta
    )
  })

The error event is documented as covering invalid user input and no video found, and the progress payload gives percentComplete and eta, which is what you need to drive a progress bar. Because the object is a child process wrapper, cancel is available, and the cancelled event is distinct from end and complete, so a cancelled job does not look like a finished one. The examples folder carries the working versions of this pattern as spawn.js, along with exec.js and run.js, and three separate files for the HandbrakeCLI path override: environment variable, spawn and run.

exec and run are the same thing with a different return convention

For short jobs, waiting for completion is more convenient than listening for events, and that is what exec and run are for. exec is exported and synchronous, runs HandBrakeCLI with the supplied options, and calls the callback on completion with the signature onComplete(err, stdout, stderr), where stdout and stderr are strings containing the HandBrakeCLI output. Its documented use is short duration tasks where you can wait until completion for the output. run is identical to exec except that it returns a promise instead of invoking a callback, and it fulfils with an object containing the output in two properties, stdout and stderr. The example given for exec is a preset listing:

js
import hbjs from 'handbrake-js'
hbjs.exec({ preset-list: true }, function(err, stdout, stderr){
  if (err) throw err
  console.log(stdout)
})

And the promise form is a single await:

js
import hbjs from 'handbrake-js'
const result = await hbjs.run({ input: 'input.mov', output: 'output.mp4' })

The choice between them is stylistic, and the documentation is clear about when each applies. Use exec when you do not need the progress events reported by spawn, and run when you would rather not thread a callback through your code. Since the HandbrakeCLI options are passed straight through, a preset list call is a legitimate use of the same interface as a full transcode.

Installing it, and the one platform that needs help

On Mac and Windows only Node.js is required, because everything else is installed automatically, and the postinstall script in the package manifest is what does it. On Ubuntu you have to install HandBrakeCLI yourself:

bash
$ sudo apt install handbrake-cli

Users of other Linux distributions can install the GUI and the CLI together using flatpak. Once the HandBrakeCLI binary is installed, Linux and Docker users might need to specify its path to handbrake-js, which is the detail that trips people up in containers. You can point at the binary in an environment variable:

sh
HANDBRAKECLI_PATH="./example/HandbrakeCLI"

or pass HandbrakeCLIPath as an option programmatically, which the spawn and exec tables both document as a parameter that overrides the built-in binary path. As a library, the install is npm install handbrake-js --save. As a command line app, npm install -g handbrake-js puts a handbrake command on your path, and you can then call it as you would HandbrakeCLI with all the usual options. By default only statistics are output, and passing --verbose prints the raw HandBrakeCLI output. The example given transcodes an AVI to the more universally compatible H.264 mp4, with a preset of Normal.

What the wrapper does not give you

The boundary here is worth stating plainly, because the name suggests more than it delivers. This is a process wrapper, not an encoder. The actual transcoding is done by HandBrakeCLI, a separate binary, and the module's job is to find it, start it with your options, and turn its output into events, a callback or a promise. The dependency list reflects that job, with command-line-args and object-to-spawn-args for turning your options into a command line, current-module-paths for locating the binary, node-fetch and decompress for the download that the postinstall script performs, and ansi-escape-sequences for handling the coloured output. The option pass-through is the double edged part. It means the library stays small and never lags HandBrake, and it also means there is no typed, validated configuration layer, so a typo in an option name is discovered by the CLI rather than by your editor. The other limit is that this is a one-shot converter, not a media pipeline. There is no queue, no retry, no concurrency control, and the progress reporting is a callback you would have to turn into all of that yourself. If you need a transcoding service with job scheduling, you are still writing that layer; what this library removes is the shell quoting and output parsing.

Version 8 changed the shape of the package

The release history is short and the gap between the versions is the story. Version 7.0.0 was published in May 2023, 7.1.0 in July 2024, and 8.0.0 in May 2026, and the last push to the repository was on 2026-05-09 with the package version at 8.0.2. The documentation opens with a note addressed to upgraders pointing at the release notes, which is the correct signal that something structural moved. The package manifest shows what. The type field is module, and the exports map has an import entry pointing at index.js and a require entry pointing at dist/index.cjs, and the dist build is produced by a cjs-build script, so both module systems are supported. The shipped files are bin/cli.js, lib, dist, scripts and index.js, and the CLI entry is registered as handbrake in the bin map. The runtime dependencies are current, with node-fetch at 3, command-line-args at 6 and current-module-paths, and the peer dependency on @75lb/nature is marked optional, which is the test and tooling package the dev scripts use, with the test script running a named list of test files through a test runner. The package manifest declares MIT, with the licence file named LICENCE at the repository root, and the project is tested on Mac, Ubuntu and Windows.

Editorial conclusion

Adopt handbrake-js if your Node service needs to transcode video and you would rather drive the HandBrake CLI through events and a promise than parse command output yourself. Do not adopt it if you need fine control over the encoder, because the options object is passed straight to HandBrakeCLI, and do not assume a Linux container comes with the binary. Before you deploy, install handbrake-cli on Linux yourself, set the path to it explicitly if the auto detection cannot find it, and read the release notes, since version 8 is a major release and the module format changed.

Frequently asked questions

What does handbrake-js do?

It is a Node.js wrapper around HandBrake that provides three ways to run the HandBrakeCLI binary: spawn, which returns a Handbrake instance with progress events, exec, which takes a completion callback, and run, which returns a promise of stdout and stderr. The options you pass go straight to HandBrakeCLI.

How do I install handbrake-js on Linux?

On Ubuntu you must install the CLI yourself with `sudo apt install handbrake-cli`, since the automatic install covers only Mac and Windows. On other distributions the GUI and CLI can be installed together with flatpak, and Linux or Docker users may need to point at the binary with the HANDBRAKECLI_PATH environment variable or the HandbrakeCLIPath option.

Which codecs does handbrake-js support?

It supports HandBrake's own output set, since it drives HandBrakeCLI. Containers are .MP4 (.M4V) and .MKV, video encoders include H.264 with x264, H.265 with x265, MPEG-4 and MPEG-2 with libav, VP8 with libvpx and Theora with libtheora, and audio encoders include AAC, MP3, Flac, AC3 and Vorbis, with audio pass-through for AC-3, DTS, DTS-HD, AAC and MP3 tracks.

Can I use handbrake-js as a command line tool?

Yes. A global install with `npm install -g handbrake-js` registers a handbrake command that behaves like HandbrakeCLI with the usual options. By default only statistics are printed, and passing --verbose prints the raw HandBrakeCLI output.

What is the difference between handbrake-js spawn, exec and run?

spawn is for long jobs and returns a Handbrake instance emitting progress, output, error, end, complete and cancelled events, and it offers a cancel method. exec runs the process and calls a callback with err, stdout and stderr on completion, which suits short jobs. run is identical to exec but returns a promise instead of invoking a callback.

Official sources

  1. 75lb/handbrake-js on GitHub
  2. Issues
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/75lb-handbrake-js.svg)](https://hysenlabs.com/projects/75lb-handbrake-js)

Was this useful?