Article Categories
- All Categories
-
Data Structure
-
Networking
-
RDBMS
-
Operating System
-
Java
-
MS Excel
-
iOS
-
HTML
-
CSS
-
Android
-
Python
-
C Programming
-
C++
-
C#
-
MongoDB
-
MySQL
-
Javascript
-
PHP
-
Economics & Finance
Streaming a video file to an HTML5 video player with Node.js so that the video controls continue to work
When streaming video files to an HTML5 video player with Node.js, it's essential to implement proper HTTP range request support to maintain video controls functionality. The HTML5 video element relies on partial content requests (HTTP 206) to enable seeking, scrubbing, and progressive loading.
How HTTP Range Requests Work
The browser sends a Range header (e.g., bytes=0-1023) to request specific portions of the video file. The server responds with a 206 Partial Content status and the requested byte range, allowing the video player to load segments on demand.
Basic Video Streaming Setup
Following example demonstrates the basic structure for streaming video with range request support −
<!DOCTYPE html>
<html>
<head>
<title>Video Streaming Example</title>
</head>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h2>HTML5 Video with Node.js Streaming</h2>
<video width="640" height="360" controls>
<source src="/video/sample.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<p>The video controls work properly with range request support.</p>
</body>
</html>
Node.js Server Implementation
The server must handle range requests and respond with appropriate headers. Here's the complete implementation using createReadStream() −
const fs = require('fs');
const path = require('path');
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url.startsWith('/video/')) {
const videoPath = path.join(__dirname, 'videos', 'sample.mp4');
// Check if file exists
if (!fs.existsSync(videoPath)) {
res.writeHead(404);
res.end('Video not found');
return;
}
const stat = fs.statSync(videoPath);
const fileSize = stat.size;
const range = req.headers.range;
if (range) {
// Parse range header
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;
// Create readable stream for the requested range
const stream = fs.createReadStream(videoPath, { start, end });
stream.on('open', function () {
res.writeHead(206, {
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": "video/mp4"
});
stream.pipe(res);
});
stream.on('error', function(err) {
res.end(err);
});
} else {
// No range header - send entire file
res.writeHead(200, {
"Content-Length": fileSize,
"Content-Type": "video/mp4"
});
fs.createReadStream(videoPath).pipe(res);
}
}
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Key Implementation Details
The critical aspects of proper video streaming implementation include −
Range Header Parsing − Extract start and end byte positions from the
Range: bytes=start-endheader.206 Status Code − Return
206 Partial Contentstatus for range requests instead of200 OK.Content-Range Header − Specify the exact byte range being sent in format
bytes start-end/total.Accept-Ranges Header − Indicate that the server supports range requests with
Accept-Ranges: bytes.Correct Content-Type − Use
video/mp4instead ofnew/mp4for proper MIME type.
Enhanced Server with Error Handling
Following is an improved version with comprehensive error handling and CORS support −
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
// Enable CORS for video requests
app.use('/video', (req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Range');
res.header('Access-Control-Expose-Headers', 'Content-Range, Content-Length');
next();
});
app.get('/video/:filename', (req, res) => {
const videoPath = path.join(__dirname, 'videos', req.params.filename);
fs.stat(videoPath, (err, stats) => {
if (err) {
console.error(err);
return res.status(404).end('Video not found');
}
const { size } = stats;
const range = req.headers.range;
if (range) {
let [start, end] = range.replace(/bytes=/, '').split('-');
start = parseInt(start, 10);
end = end ? parseInt(end, 10) : size - 1;
if (start >= size || end >= size) {
return res.status(416).end('Range Not Satisfiable');
}
const contentLength = end - start + 1;
const stream = fs.createReadStream(videoPath, { start, end });
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
'Content-Length': contentLength,
'Content-Type': 'video/mp4',
});
stream.pipe(res);
} else {
res.writeHead(200, {
'Content-Length': size,
'Content-Type': 'video/mp4',
});
fs.createReadStream(videoPath).pipe(res);
}
});
});
app.listen(3000, () => {
console.log('Video streaming server running on port 3000');
});
Testing the Implementation
To test that video controls work properly, verify these behaviors −
Seeking − Click anywhere on the progress bar to jump to that position
Scrubbing − Drag the progress handle back and forth smoothly
Progressive Loading − Video starts playing before fully downloaded
Network Efficiency − Only requested segments are downloaded
Check browser developer tools to confirm 206 Partial Content responses and proper Content-Range headers.
Common Issues and Solutions
| Issue | Solution |
|---|---|
| Video won't seek/scrub | Ensure 206 status code and proper Content-Range header |
| Controls don't work | Check Accept-Ranges: bytes header is present |
| Video won't start | Verify correct Content-Type (video/mp4, not new/mp4) |
| Range errors | Validate start/end values don't exceed file size |
Conclusion
Proper video streaming with Node.js requires implementing HTTP range request support using createReadStream() with start/end options. The server must respond with 206 Partial Content status and correct headers including Content-Range and Accept-Ranges to ensure HTML5 video controls function properly for seeking, scrubbing, and progressive loading.
