If you not sure how stream and buffer works. Make sure you checkout my previous article which talks about various methods from stream.
Stream a video can help us speed up the waiting process which will naturally improves the user experience as well
First of all, let’s start a simple express server in port 3000 which will serve the index.html file.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Streaming App</title>
<style>
body{
margin: 50px auto;
max-width: 800px;
background-color: rgb(56, 255, 255);
font-family: sans-serif;
}
</style>
</head>
<body>
<h1>Video Streaming App</h1>
<video id='videoPlayer' width="650" controls muted="muted" autoplay>
<source src="/video" type="video/mp4"/>
</video>
</body>
</html>
Result
This article uses a Big Buck Banny video for demo. You may get more public test video from https://gist.github.com/jsturgis/3b19447b304616f18657
server.js
const express = require('express');
const app = express();
const fs = require('fs');
//Serve static file
app.get("/", (req, res) => {
res.sendFile(__dirname + "/index.html")
})
app.get("/video", (req, res) => {
// Need range header for telling client which part of the video we want to send back
const range = req.headers.range;
if (!range) {
res.status(400).send("Require Range Header");
return
}
const videoPath = 'bigbuck.mp4';
const videoSize = fs.statSync(videoPath).size;
const CHUNK_SIZE = 10 ** 6 // 10 power of 6 is 1MB
const start = Number(range.replace(/\D/g, ""));// replace all non-digit characters to empty string and parse into Number
// calculate the ending byte we want to response back to client. If it reached the end of file (total video size)
//Then send back the video size instead
const end = Math.min(videoSize - 1, start + CHUNK_SIZE);
const contentLength = end - start + 1;
const header = {
"Content-Range": `bytes ${start}-${end}/${videoSize}`,
"Accept-Range": "bytes",
"Content-Length": contentLength,
"Content-Type": "video/mp4",
};
res.writeHead(206, header)
const readStream = fs.createReadStream(videoPath, { start, end });
readStream.pipe(res)
})
const PORT = process.env.PORT || 3000
app.listen(PORT, () => { console.log(`Server is Running at PORT ${PORT}`) })
Result
Kindly open the url http://localhost:3000
and open Developer tool(F12)
-> Network Tab
-> Filter by Media
Explanation
- As we can see from the video, when we jump to certain time frame of the video, it will immediately load a chunk of stream from the server to ensure the video can be play smoothly.
- First of all, we need to get the value from Range HTTP Request Header so that we know what is the playing time of the video.
- Example of range header:
bytes=4065540-
- Example of range header:
- Define the video path , size and total chunk size of the document to be loaded and response to client later. This article uses 1MB for the chunk size
- There are several headers we need to define before we response back to the client
- Content-Range: indicates where in a full body message a partial message belongs
- Accept-Range: allow us to resume an interrupted download, rather than to start it from the start again.
- Content-Length: size of message body (bytes) we are send back to client
- Content-Type: MIME type of the response
- Response with the status code 206 which indicates it’s partially content
- Create a readable stream and read the file by providing the video path, start and end point. In the meantime, we can response back to the client with writable stream by using
pipe()
method