Boldizsár's programming blog

Video streaming with NodeJS

June 29, 2019 | 7 Minute Read

NodeJS can easily be used to stream your videos. You can find out more below.

What you should know before looking at the code

Video files can be large. It’s not a good idea to download them as a whole. If you look at YouTube or Netflix you can see that the videos are downloaded in chunks. Why? Because first, you can start viewing the content sooner since as soon as the first chunk arrives you can see something. Secondly, what if you only watch 5 seconds from a video, in that case it would have not really been worth it to download all of it. It would’ve meant more pressure on the server and the network. That’s cool but this is something everyone knows I guess so why am I even talking about that? Well, the question here is how does HTTP support this solution. How does the server tell the client (browser) that it’s sending something in chunks so it should expect more. Now comes HTTP status 206 into the picture.

The Request

The HTTP 206 Partial Content success status response code says that the request has been successful and the body contains the a requested ranges of data. Okay since we’re talking about requested range then it implies the client (browser) requests some parts of the content. Yes, this is true. The client does so by using the Range request header.

The Range request header has the following format (I’m listing the two which we’ll encounter now):

<unit>=<range-start>-

<unit>=<range-start>-<range-end>

At the beginning the first format we’ll be used which is quite trivial since the client has no clue how big the file is. This is how the first request looks in Chrome, a request for a video file.

As you can see the Range header specified that the client wants the data from the beginning. Later when the client knows how big the content is then the Range header will have the second format where it states the end boundary as well.

The Response

How should the response look like? There are a couple of headers we have to include in our response.

  • Accept-Ranges: This is not compulsory, but we can state that the unit of the range is bytes
  • Content-Type: We have to specifies the MIME type of the file we’re sending. In our example it is video/mp4
  • Content-Range: This header tells the client what part of the media content is currently sent down. The format in our example: <unit> <range-start>-<range-end>/<size>
  • Content-Length: The size of the chunk currently being sent
The NodeJS part

Fortunately with the built in fs module it is very easy to deal with the data and get all the information necessary. With the fs.createReadStream function we can open a Readable stream of our file which then we can pipe to the response object. In our example we’re going to use express which uses the built in http module’s ServerResponse object. This object inherits from the Stream so we can pipe to it. We can use the fs.statSync function to get the file size.

Let’s see the code

// server.js

const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();

app.use(express.static(path.join(__dirname, 'public')));

const videoFilePath = path.join(__dirname, 'vid.mp4');
const fileSize = fs.statSync(videoFilePath).size;

app.get('/video', (req, res) => {
  const { range } = req.headers;
  const [s, e] = range.replace('bytes=', '').split('-');
  const start = Number(s);
  const end = Number(e) || fileSize - 1;
  res.append('Accept-Ranges', 'bytes');
  res.append('Content-Range', `bytes ${start}-${end}/${fileSize}`);
  res.append('Content-Length', end - start + 1);
  res.append('Content-Type', 'video/mp4');
  res.status(206);
  const fileStream = fs.createReadStream(videoFilePath, { start, end });
  fileStream.pipe(res);
});

app.listen(3000, () => {
  console.log('Server is up...');
});

So I’m importing a bunch of dependencies. Then telling express to serve the public folder which contains the static content. Then I create the path to the video file so that I can get the file size. In the /video endpoint I’m extracting the Range header then I’m destructing the start and end values. As you can see if no end value is specified I’m using the fileSize - 1 (since the counting starts from 0). Then I’m adding a bunch of headers to the response with the status code 206. Using the fs.createReadStream I create a Readable stream and specify which part of the file I want to read. At the end I pipe the streams.

Let’s look at the frontend code.

<!--public/index.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Video  test</title>
    <link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
    <h1>Watch this video</h1>
    <video controls width="800">
        <source type="video/mp4" src="http://localhost:3000/video">
    </video>
</body>
</html>
/*public/index.css*/

body {
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

It’s quite easy as you can see. We only need a video tag (controls attribute means the browser will show the play, pause, etc buttons). Inside a source tag is needed to specify the url in our case. I added some styles as well so the content is both horizontally and vertically centred but it’s not necessary. If you run the app now you’ll see this.

As you can see by the bar in the botton of video that only some part of the content has been fetched. If you press play and look at the network tab you’ll see the data downloaded in chunks.

The end

In this article you saw how easy it was to stream videos with NodeJS. You can find the code here. If you liked this article, please press the like button below and share this article. See you next time.