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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constexpress=require('express');
constapp=express();
constserver=require('http').Server(app);
constio=require('socket.io')(server);
constconfig=require('config');
constPORT=config.get('port');
app.set('views',__dirname+'/views');
app.set('view engine','ejs');
app.use('/',express.static(__dirname+'/public'));
app.get('*',(req,res)=>{
res.render('app');
});
server.listen(PORT,()=>console.log('Server running on Port: '+PORT));
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.
1
2
3
4
5
6
7
8
9
10
11
<!doctype html>
<html lang="en">
<head>
<title>nverter</title>
<link rel="stylesheet"href="/style.css">
</head>
<body>
<div id="root"></div>
</body>
<script src="/bundle.js"></script>
</html>
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.
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
onFileChange(e){
if(!e.target.files.length){
return;
}
let file=e.target.files[0],
ext=this.getFileExtension(file.name);
if(this.validateFile(ext)){
this.setState({
file:file,
upload_ext:ext
})
}else{
toastr.error('Error: Invalid file format')
}
}
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.
1
2
3
4
5
6
7
getFileExtension(name){
return/(?:\.([^.]+))?$/.exec(name)[1];
}
validateFile(ext){
returnthis.state.allowed_types.includes(ext);
}
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.
1
2
3
4
5
6
7
8
9
10
cancelUpload(e){
this.setState({
file:null,
uploading:false,
progress:0,
upload_ext:null,
convert_ext:'',
});
this.fileInput.current.value='';
}
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.
1
2
3
4
5
6
7
8
9
10
11
12
constmulter=require('multer');
conststorage=multer.diskStorage({
destination:function(req,file,cb){
cb(null,path.resolve(__dirname,'uploads'))
},
filename:function(req,file,cb){
cb(null,Date.now()+'_'+file.originalname)
}
});
constupload=multer({storage:storage});
We have configured
multer to store uploaded file in the uploads directory. A date will be concatenated with uploaded file names.
let moveUploadedFileToUserDir=(upload_path,move_path,filename,res)=>{
fs.rename(upload_path,move_path,(err)=>{
if(err)throwerr;
res.json({
uploaded:true,
path:filename
});
});
};
let createUserEncodeDir=(user)=>{
let dir=path.join(__dirname,'/encoded/',user);
fs.exists(dir,(exists)=>{
if(!exists){
fs.mkdir(dir,function(err){
if(err){
returnconsole.error(err);
}
});
}
});
};
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.
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}/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
export defaultclassHistoryextendsReact.Component{
constructor(props){
super(props);
this.state={
encodes:[]
}
}
componentDidMount(){
get('/history')
.then(res=>{
console.log(res.data);
this.setState({
encodes:res.data
});
});
}
render(){
let uid=Cookie('_uid');
return(
<div className="encodes">
<h2>
EncodeHistory
</h2>
{this.state.encodes.map(encode=>{
return(
<div key={encode}className="encode">
{encode.substring(encode.indexOf('_')+1)}
<ahref={'/encoded/'+uid+'/'+encode}
className="download"
download>
Download
</a>
</div>
)
})}
{!this.state.encodes.length&&(
<div className="message">
No Encodes Found
</div>
)}
</div>
)
}
}
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
app.get('/history',(req,res)=>{
let dir=path.join(__dirname,'encoded',req.cookies._uid);
fs.exists(dir,(exists)=>{
if(exists){
fs.readdir(dir,(err,files)=>{
if(err)throwerr;
res.json(files.reverse());
});
}else{
res.json([]);
}
});
});
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
8 thoughts on “Building a Video Converter App with Node.js, Express and React”
Getting error when ran code from Github link
The “path” argument must be of type string. Received type undefined
So I followed the directions per github and everything was working until the ‘npm run start’ command. Up until this point, the front end code built with no errors, and the system let me know there were multiple mongod processes running. I killed them all because I got an internal server error(500) on the browser and only ran ‘node server.js’ I then received the error mentioned in the comment above ‘The “path” argument must be of type string. Received type undefined’. So I’m thinking that I need to correct the ‘path.join’ syntax? I’m creating the upload folder fine but I am not able to see an uploaded file the history in the ‘encode history’ button. Also, nothing is being converter. Any help or direction would be greatly appreciated. I’m very interested in this project.
Getting error when ran code from Github link
The “path” argument must be of type string. Received type undefined
Where, upon doing what and how do I regenerate it? Node version?
I received the same error.
So I followed the directions per github and everything was working until the ‘npm run start’ command. Up until this point, the front end code built with no errors, and the system let me know there were multiple mongod processes running. I killed them all because I got an internal server error(500) on the browser and only ran ‘node server.js’ I then received the error mentioned in the comment above ‘The “path” argument must be of type string. Received type undefined’. So I’m thinking that I need to correct the ‘path.join’ syntax? I’m creating the upload folder fine but I am not able to see an uploaded file the history in the ‘encode history’ button. Also, nothing is being converter. Any help or direction would be greatly appreciated. I’m very interested in this project.
Me too.
It is looking for socker user that is comming from Cookies, just make a small fix for this:
js
io.on('connection', (socket) => {
socket.on('encode', (data) => {
let handbrake,
completed = false,
file = data.file,
user = socket.handshake.headers?.cookie.substring(5, 41), // <==== Change it
...
Use http://127.0.0.1:port instead of localhost:port or lvh.me:port. There is hardcoded domain : ‘127.0.0.1’ in userCookie.js file.
Great post … i will try to adapt it in a Nestjs Backend I am building now.
Love your guides man, keep them coming!