Building a Video Converter App with Node.js, Express and React

In this tutorial, I’ll show you how to build a video encoder with Node.js. We will use express.js on the backend to run a web server, multer for uploading files and socket.io for broadcasting real-time encoding progress to the user. We will use handbrake-js to encode videos which under the hood, spawns HandBrakeCLI process and lets us listen to events. We will use React to build our Frontend where a user can upload videos, view real-time encoding progress, download and view their converted videos.

Getting Started

I’m going to split this article into three parts. In the first part, we will create a simple web server and setup basic user identification. In the second part, we will write our uploader components and implement backend logic for uploading videos using multer. In the last part, we will build an encoder component and implement backend logic for encoding videos and broadcast real-time progress to the user through WebSockets.

Here’s a demo of how our app will look and function at the end of this tutorial.

Web Server

Let’s start by creating a bare-bones HTTP server. In your working directory create a  server.js file and add this code.

I’m passing HTTP server to socket.io to hook them together and run on the same port. Since we are using react and react-router on the frontend, we will bootstrap our SPA inside app view and render it against every incoming request. Add an app.ejs file under your views directory and add this code.

User Identification

Since we are not authenticating users, we will simply create a cookie in their browser to identify them. Let’s create a middleware to store a unique id cookie in the user’s browser.

Create  userCookie.js file under the middleware directory and add this code.

Express will run this middleware for every incoming request. It will create a unique id cookie (_uid) in the user’s browser if it doesn’t exist.

Uploader

Create a Uploader.js file under the src directory. We will place all our components under this directory along with the CSS files.

In Uploader component, I’ve set up an initial state and bound event handlers. Initially, we will display a select video file button, onClick it will trigger a click on a hidden file input and opens up a file dialog.  On file select, we are calling  onFileChange method. Let’s create it.

In this method, we are validating if a user has selected a file. We will then parse out selected file extension and check if exists in the allowed_types array. Add these two methods to your component.

getFileExtension method will parse out file extension from its name using regex and validateFile method will lookout for extension in the allowed_types array in component’s state. If the file passes these validation checks, we will update file and upload_ext state on our component. After updating the state, we’ll display Upload and Cancel buttons. A select drop-down will be populated with available formats for video encoding. We will filter out the extension of the file that the user has selected. On cancel, we will reset the component state by calling cancelUpload method.

We have an  uploadFile method for handling Upload button clicks.

In this method, we will validate if there’s a file present and the user has selected an extension from the drop-down input. After passing this validation check, we will create a FormData() object and append file and ext to it. We will also set uploading state to true which will render  Progress component. Progress is a functional component that takes in percentage value as a prop and renders a progress bar.

We are using axios to upload the file. We are passing  onUploadProgress method with config to axios which listens to progress event. We will use it to calculate the upload percentage and update component progress state which will result in re-rendering of Progress component.

Let’s configure multer storage and handle upload request on the backend.

We have configured multer to store uploaded file in the uploads directory. A date will be concatenated with uploaded file names.

multer middleware will make upload file available through req.file. After upload, we will move the file to the user’s upload directory. We will retrieve the user’s unique id from req.cookies and use it to create a user directory and move uploaded file to it. On success, we will return filename as a response.

initEncoding method is being passed from App component. Let’s create our App component and define this method.

In our App component, we will initially render Uploader component and when it calls our initEncoding prop method, we will update its state. We are passing file and convert_ext as props to our Encoder component.

Encoder

After Uploader component calls initEncoding prop method on successful upload, App component will render Encoder component.

After mounting, we will open a WebSocket connection and emit an encode event. Let’s handle this socket connection and event on the backend. Add this code to your server.js file.

We will spawn a separate HandBrakeCLI process for every connected socket. If socket disconnects before completion, we will stop handbrake process and delete uploaded and incomplete encoded video. On handbrake progress event, we’ll emit progress event and pass progress percentage and eta with it. On complete event, we will emit a complete event with the encoded file name. On the front end, we are listening to these WebSocket events and update encoded_file state on completion. After updating state, a download button will be displayed to the user for downloading their encoded file.

Since we are using react-router, we will render History component for /encode path and render a Link in Navbar that points to it.

<Route exact path="/encodes" component={History}/>

When it renders, we will make a get request to retrieve user’s encoded files. We will render them in a list with download buttons. Add this router handler on the backend to return encoded files.

A lot of small details like CSS styles and webpack config are missing from this tutorial. I’ve created a Github repository with complete code and setup instructions. If you get any errors, please mention them in comments or open a Github issue.

Share this post

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.