How to Use Node.js Workers for Video Encoding

Video encoding is very CPU-intensive, but let’s look at how to do this anyway

How to Use Node.js Workers for Video Encoding
Photo by sol on Unsplash

Last night, I again worked on my side project, Mini Video Encoder. I added a new service for encoding video. The Workflow Encoder, as I call the service, uses Node.js. You may know that video encoding is very CPU-intensive.

In the past, Node.js has never been a good candidate for performing CPU-intensive tasks. The reason for this is Node.js uses a single thread to execute JavaScript code. But since the release of 11.7.0, Node.js has a new module called worker_threads.

The Node.js team describes Workers like this:

“Workers (threads) are useful for performing CPU-intensive JavaScript operations. They will not help much with I/O-intensive work. Node.js’s built-in asynchronous I/O operations are more efficient than Workers can be.”

Because of this, I implemented the Workflow Encoder using Workers. In this piece, I describe the implementation.


Encoding Video

Before describing the implementation, I need to talk a bit about why encoding is necessary for streaming video.

The Workflow Encoder is part of a larger project called Mini Video Encoder (MVE). MVE is an open-source platform for converting videos. After conversion, a regular HTTP server can deliver the videos using adaptive streaming.

What is adaptive streaming?

Adaptive streaming changes the bit rate and resolution of a video during playback. A video player continuously measures the bandwidth of the connection and increases or decreases the quality of the video.

To make this possible, Workflow Encoder creates many versions of the same video. Each version has a different bit rate and resolution. This list of bit rates and resolutions is called an encoding ladder.

Apple recommends the following encoding ladder for a 1080p video. If you encode your videos, use this ladder — the video will play correctly on Apple devices.

Using this ladder means the Workflow Encoder has to create nine different encodings. So you understand why we need to use the most efficient method for video encoding.

The Workflow Encoder uses FFmpeg 4.2.2. FFmpeg is an open-source video and audio encoder. It also uses the Fluent ffmpeg-API to make interacting with FFmpeg easier.

For testing, I used the 1080p version of Caminandes 3: Llamigos. Caminandes 3 is a funny little open-source animation video of 2.5 minutes from the Blender Institute.

Caminandes 3, the video used for testing video encoding

Implementing the Workflow Encoder Using Workers

When the Workflow Engine receives a video job, it first splits the job. For each bit-rate and resolution combination from the encoding ladder, the Workflow Engine creates a task.

The Workflow Encoder communicates with the Workflow engine and asks if there’s a task to perform. The Workflow Encoder uses the REST API of the Workflow engine for communication.

Creating and starting a Worker

If there’s a task to perform, the Workflow Encoder calls the function startEncoder. The startEncoder function creates and starts the Worker. It creates the Worker object by calling the constructor and passing a relative path to a JavaScript file. This file contains the function that must be executed on a different thread.

The second argument of the Worker constructor is an options object. I use this to set workerData to the encodingInstructions. This assignment clones encodingInstructions and makes it available in the Worker function.

The actual function performing the work is encode in encoder.js. The file contains a single function that the Worker executes. I left out most to focus only on the essential part.

On line 5, I get the encodingInstructions by reading workerData. At the start of the file, I also require parentPort, which the function uses to perform communication between the main thread and the Worker thread.

How to communicate from the Worker to the main thread

The Worker module allows bidirectional communication between the main and worker thread. I’d like to get feedback from the Worker thread on the main thread about the progress.

Most examples use postMessage to send a string. Instead, I communicate an object. I want to send different messages and be able to distinguish them in the main thread.

The main thread, on the other end, receives these messages through events. On line 8, worker.on defines the function that receives the message events.

Depending on the type of message, the function performs a specific action. In the case of the PROGRESS message, it logs the message using the log object. This way, we see the progress of the worker in the main thread.

The Worker reports the progress to the main thread

How to communicate from the main thread to the Worker

We also want to be able to communicate the other way around, from the main thread to the Worker — for example, when we want to stop a running encoding task.

The mechanism is almost the same as communicating from the Worker to the main thread. Here, we use the postMessage method on the worker object.

The Worker uses the parentPort to create an event handler for receiving messages.

When the Worker receives STOP_ENCODING, it stops the running encoding task. It stops the task by calling ffmpegCommand.kill(). This will SIGKILL to the FFmpeg process and stop it.

Worker thread stops when it receives a stop message from the main thread

Conclusion

I like how the Node.js team implemented threading using Workers. By having an explicit communication channel between threads, they prevent synchronization issues. Synchronization causes a lot of issues with other programming languages.

The current implementation of the Workflow Encoder uses Workers for encoding video. You can find the source on GitHub. It’s still a work in progress.

Thank you for reading.