Video stream with Node.js and HTML5

I was asked how to do video stream using Node.js, something I had yet to try so, “why not” I thought, let me share my findings.

The “challenge” was to make a route sending a .mp4 file to a page and make the video available to be seen. The way I decomposed it:

  • Make a server route to feed the video
  • Use HTML5 and JS to request the feed
  • Make the video load in “parts” instead of loaded from the start
TL;DR — You can find a working demo of video streaming here.

Streams

Videos work by streaming, meaning that instead of sending everything to the front end in a single package you should send it little chunks at the time.

Sending it by streams would mean that instead of having to wait for the page to download the whole video from the server before being able to watch it you can instead request the server for the first few seconds of the video and download the rest while it plays out.

You can also use this approach for sending big chunks of text for example making the client wait less for the first few lines of an article to show up.

Some “theory”

  • Getting the file size: fs in node has a method called statSync that will return the stats of a file, among those stats it’s the file size which we need to know when the current loaded chunk has reached the end of the file. You can also use stat, in my case I tried to avoid synchronicity to try to keep the code easier to understand for newcomers;
  • Creating a stream from a file: fs contains another method called createReadStream which will create a stream given a file, the start and end chunks.
const fileChunk = fs.createReadStream(sample.mp4, {start, end});
  • The size of the chunks: The starting chunk will be made available to you in the request, to know how much of the file to load I subtract the end chunk size (when not available use the complete file size) with the starting chunk size.
                          endChunk - startChunk
  • HTTP 206: This is used for partial content which is what we want has the header of our connection since we are continuously feeding the front end with chunks and we want to have available our starting chunk when a request is made. You have to define in the least:
'Content-Range': 'bytes chunkStart-chunkEnd/chunkSize'
'Accept-Ranges': 'bytes'
'Content-Length': chunkSize
'Content-Type': 'video/mp4'

The server

Taking those things into account I ended up with something like this in my route named video (To create the route I’m using express)

app.get('/video', function(req, res) {
const path = 'assets/sample.mp4'
const stat = fs.statSync(path)
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)
}
});

That is a bit of code to take in, don’t worry, you can always debug it more easily with the demo. Let me try to explain the flow:

  • When a request is made we get the file size and send the first few chunks of the video in the else statement;
  • When we start watching the video (by accessing the route via localhost:3000/video or from the front end), subsequent requests are made, this time with the range in the header, meaning that we know the starting point of our next chunk;
  • Read the file again to create another stream, passing along the new value for the start and end (which will most likely be the current part that came in the request header and the file size of the video)
  • We set our 206 header response to send only part of our newly made stream by applying the formula we talked earlier.

The front end

The front end is surprisingly easy with HTML5 <video> tag you just need to add a source route and it will handle the rest for you.

<video id="videoPlayer" controls>
  <source src="http://localhost:3000/video" type="video/mp4">
</video>

The controls attribute allows you to see the players controls

Player controls, volume, play button and other

Without it you can instead program those and other properties yourself by accessing the player element in this case the id videoPlayer . SO if you have a button on your HTML you can do something like this to replicate the play/stop button:

const VP = document.getElementById('videoPlayer') // player
const VPToggle = document.getElementById('toggleButton') // button
VPToggle.addEventListener('click', function() {
if (VP.paused) VP.play()
else VP.pause()
})

In the network tab on you developer tools you can see the streaming in chunks, specially if you throttle your connection

Partial example of the stream in a 30MB file

Closing remarks

I was not expecting for this to go so smoothly for a simple implementation, there are flaws in this implementation of course like the starting chunks not always being what is expected (probably because of connection hanging since I am not handling that in this sample).

Nonetheless I find it to be a good starting point for those wanting to start doing some kind of streaming application. If you have a better or less error prone approach let me know!

Like pointed at the earlier, you can find a working demo for you to play around in github here.

Article 6 of 30, part of a project for publishing an article at least once a week, from idle thoughts to tutorials. Leave a comment, follow me on Diogo Spínola and then go back to your brilliant project!