Streaming is the process of consuming data continuously in part without downloading the entire file at once to your device and then using it afterward.
Think of YouTube, Netflix, and Amazon Prime, these platforms allow you to watch videos without first downloading the entire video to your device.
Imagine having to download all the videos you see on social media before watching them. That will be so much work, a waste of storage space, inconvenient, and even worse when you have slow internet? You’ll feel like throwing your phone away. We’ve all been there. But streaming helps with solving that problem even when there is low network latency.
In this article, we’ll see how to build a simple streaming application with NodeJS.
All you need to follow along is basic NodeJS experience.
Install Express, Morgan, and create a server
Express.js web framework is pretty popular in the Node.js community, it’s one of the widely used Node.js web servers. So, I believe it’ll be a lot easier to follow along. Also, we’ll use morgan so we can see the request logs in the console when they are made.
Create a directory for this project and initialize a Node.js project with Yarn, and create an app.js file with the code below:
mkdir videostreaming && cd videostreaming && yarn init -y && yarn add express morgan && touch app.js
When you run the code above in your Terminal your current directory structure will look like so;
.
├── app.js
├── package.json
├── node_modules
└── yarn.lock
We’ll do all the coding in the app.js
file, so open it with your favorite code editor.
Setup Express and Morgan middleware
//app.js
const Express = require("express");
const fs = require("fs");
const morgan = require('morgan');
const app = Express();
app.use(morgan('tiny'));
We’ll need two routes, one route to serve our HTML and another route to serve the video.
So, let’s create them:
//app.js
const Express = require("express");
const fs = require("fs");
const morgan = require('morgan');
const app = Express();
app.use(morgan('tiny'));
app.get("/", (req, res) => {
res.sendFile(__dirname + "/index.html");
});
app.get("/video", (req, res) => {
});
app.listen({ port: 3000 }, function (err, address) {
console.log("connected");
});
Looking at the code above you’ll notice that it is looking for anindex.html
in the root directory, right? Let’s create it.
<!-- ./index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Video Streaming</title>
</head>
<body>
<video id="videoPlayer" width="650" controls muted="muted" autoplay>
<source src="/video" type="video/mp4" />
</video>
</body>
</html>
Pay attention to this part of the code;
<video id="videoPlayer" width="650" controls muted="muted" autoplay>
<source src="/video" type="video/mp4" />
</video>
Notice that it uses an HTML5 video tag, adds controls and mutes the video by default, set it to autoplay the video once the page loads, most importantly, sets the source to the /video
endpoint we specified in the API, so, that’s where the video will be streaming from.
Let’s revert back to the API — app.js
file. Much of the code we’ll write, will be in the video endpoint callback function.
How it works
Essentially, what our app will do is, get the video from file, calculate the size (to ensure the client knows the length of the content and does not request anything more than that, which will, of course, result in an error), and send back parts of the video on demand. Interestingly, you won’t need to write any code to make a request to the API to request for the video as the client handles all of that for you.
Let’s start with getting the video we want to stream, and calculating the size. Require the fs module and use the statSync
method to get the size of the video as shown below. As you can see in the code below the code is expecting a video in a /media
directory. Get an mp4 video and put it in that directory for the sake of this tutorial.
//app.js
const fs = require("fs");
app.get("/video", (req, res) => {
// file and fileSize
const filePath = "/media/video.mp4";
const { size } = fs.statSync(__dirname + filePath);
});
Remember I mentioned earlier that the client can request the video in parts? That moment when you are seeing a boring video and you just want to skip to the end to see how it ended, familiar? That’s how you can manually request partial data even though the client does it for you. The client will send a range of when you want the partial video it’s requesting to start and when it should end. The server will be able to grab this information from the range in the header as shown below;
const range = req.headers.range;
The result of this will look likebytes=0-300
, the 0
is the starting point while the 300 is the end. We need this information as a string so, we’ll use replace and split to put convert it to remove bytes=
from the string, and get each of the items as shown below:
const parts = range.replace(/bytes=/, "").split("-"),
start = Number(parts[0]),
end = parts[1] ? Number(parts[1]) : size - 1,
contentLength = end - start + 1;
Next, let’s set the headers with these parameters;
const headers = {
"Content-Range": `bytes ${start}-${end}/${size}`,
"Accept-Ranges": "bytes",
"Content-Length": contentLength,
"Content-Type": "video/mp4",
};
res.writeHead(206, headers);
Next and the final step, let’s instruct Node.js to send the partial data to the client using Node.js .createReadStream
module
// send back partial content
const Stream = fs.createReadStream(__dirname + filePath);
Stream.pipe(res);
That’s all there is to it. Here is the full source code for the app.js
file and the entire code can be found here on GitHub.
const Express = require("express");
const fs = require("fs");
const app = Express();
const morgan = require('morgan');
app.use(morgan('tiny'));
app.get("/", (req, res) => {
res.sendFile(__dirname + "/index.html");
});
app.get("/video", (req, res) => {
// file and fileSize
const filePath = "/media/video.mp4";
const { size } = fs.statSync(__dirname + filePath);
// get range from request headers
const range = req.headers.range;
if (range) {
//get the requested content length
const parts = range.replace(/bytes=/, "").split("-"),
start = Number(parts[0]),
end = parts[1] ? Number(parts[1]) : size - 1,
contentLength = end - start + 1;
// Set headers
const headers = {
"Content-Range": `bytes ${start}-${end}/${size}`,
"Accept-Ranges": "bytes",
"Content-Length": contentLength,
"Content-Type": "video/mp4",
};
// send back partial content
res.writeHead(206, headers);
const stream = fs.createReadStream(__dirname + filePath);
stream.pipe(res);
}
});
app.listen({ port: 3000 }, function (err, address) {
console.log("connected");
});
I’ll encourage you to look more into Node.js Streams and Web Streams to learn more about streaming data
Got questions? Let me know in the comments
Happy hacking!