Today we learn about Video Streaming with Node.js and HTML5. We will capture the thumbnail from the stored video and will use it as a poster to the HTML5 video tag. We will use the FFmpeg library to capture the thumbnail from the video. You can find the executable FFmpeg file at /bin directory from the source code.
Requirements
- Express
- Ejs (Template Engine)
- FFmpeg
- MP4 video
The very first step is to create a project directory and install the related dependencies through the node package manager (NPM).
npm i express ejs
Next, create a file server.js
and start creating the node server.
const express = require('express');
const router = express.Router();
const app = express();
// Set ejs template engine
app.set('view engine', 'ejs');
router.get('/', (req, res) => {
res.render('index');
});
app.use(router);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
Now create the views
directory to the root of the working directory. The views
directory holds all the ejs template files. We only required a single file to display the video player. So, let’s create an index.ejs
file at /views directory.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Video Stream App</title>
<link rel="stylesheet" href="//stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<div class="app">
<header>
<nav class="navbar navbar-dark bg-primary">
<h1 class="navbar-brand mb-0">Video Stream</h1>
</nav>
</header>
<div class="container">
<div class="row">
<div class="col-md-8 m-auto pt-4">
<h1 class="text-center mb-5">Video Stream Example Nodejs + HTML5</h1>
<video controls width="100%">
....
....
Sorry, your browser doesn't support embedded videos.
</video>
</div>
</div>
</div>
</div>
</body>
</html>
I hope everything is working fine without any error. Next, we have to make a route that will use to stream our video. Let’s name it /video and create it to the server.js
file.
....
router.get('/video', (req, res) => {
....
});
app.use(router);
....
Now, the next step is to stream the video through our /video route. We assume that you have some mp4 files stored on your desired location. We have stored at /public/assets directory on our project root.
const path = 'public/assets/nature.mp4';
fs.stat(path, (err, stat) => {
// Handle file not found
if (err !== null && err.code === 'ENOENT') {
res.sendStatus(404);
}
const fileSize = stat.size
const range = req.headers.range
if (range) {
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1;
const chunksize = (end-start)+1;
const file = fs.createReadStream(path, {start, end});
const head = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'video/mp4',
}
res.writeHead(206, head);
file.pipe(res);
} else {
const head = {
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
}
res.writeHead(200, head);
fs.createReadStream(path).pipe(res);
}
});
Very first, we need to collect some information like the size from the stored file through fs.stat()
async method. Later we are checking if the server supports the partial requests through by checking the Accept-Range in the header.
If we found the Accept-Range header then we will stream the video in chunks.
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1;
const chunksize = (end-start)+1;
const file = fs.createReadStream(path, {start, end});
const head = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'video/mp4',
}
res.writeHead(206, head);
file.pipe(res);
Now time to alter changes in index.ejs
file, which is located at /views directory and has to add the video source.
<video controls width="100%">
<source src="http://localhost:3000/video" type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
Now if you run the script and check the view at http://localhost:3000
you will see the video player with all the controls functionality like play / fullscreen etc.
But we can’t see the thumbnail on the video player, ofcause we have to add the thumbnail to the video tag through poster attribute.
Generate thumbnail from Video
It’s time to use the FFmpeg to generate the thumbnail from the video. We will use the child_process module to execute the FFmpeg command through our node application.
Let’s modify our main route i.e. route.get('/', ...);
.
To reduce the FFmpeg command execution load, We execute the FFmpeg command only once to generate the thumbnail.
const filePath = 'public/assets';
const filename = 'nature';
fs.access(filePath+'/images/'+filename+'.jpg', fs.F_OK, (err) => {
if (err) {
exec(`bin/ffmpeg -i ${filePath}/${filename}.mp4 -ss 00:00:04.00 -r 1 -an -vframes 1 -f mjpeg ${filePath}/images/${filename}.jpg`, (error, stdout, stderr) => {
if (error) {
return;
}
res.render('index', {
image: `/assets/images/${filename}.jpg`
});
});
}
if(err === null) {
res.render('index', {
image: `/assets/images/${filename}.jpg`
});
}
});
Now add the thumbnail to the video player.
<video controls width="100%" poster="<%= image %>">
<source src="http://localhost:3000/video" type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>