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-end header.

  • 206 Status Code − Return 206 Partial Content status for range requests instead of 200 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/mp4 instead of new/mp4 for 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.

HTTP Range Request Flow Browser Node.js Server Range: bytes=0-1023 206 + Content-Range: bytes 0-1023/5000000 Key Headers Accept-Ranges: bytes Content-Type: video/mp4

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.

Updated on: 2026-03-16T21:38:53+05:30

346 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements