Video streaming with HTML 5 via node.js

I'm trying to set up a web server that will support streaming video to an HTML5 video tag using node.js. Here's my code so far:

var range = request.headers.range;
var total = file.length;

var parts = range.replace(/bytes=/, "").split("-");
var partialstart = parts[0];
var partialend = parts[1];

var start = parseInt(partialstart, 10);
var end = partialend ? parseInt(partialend, 10) : total-1;

var chunksize = (end-start)+1;

response.writeHead(206, { "Content-Range": "bytes " + start + "-" + end + "/" + total, "Accept-Ranges": "bytes", "Content-Length": chunksize, "Content-Type": type });
response.end(file);

Where "request" represents the http request, type is either "application/ogg" or "video/ogg" (I've tried both) and "file" is the .ogv file that's been read from the file system. Here are the response headers:

Content-Range   bytes 0-14270463/14270464
Accept-Ranges   bytes
Content-Length   14270464
Connection     keep-alive
Content-Type     video/ogg

I've examined the response headers and this code appears to be working fine, but there are a couple of problems:

  1. The video appears to load very slowly for being on a local network. From what I can tell examining the response using firebug, the file appears to be streamed in at about 150 kb/sec.
  2. The video doesn't play at all. Even if I wait for the whole thing to load, the HTML 5 video tag just shows a big "x" instead of a movie in firefox.

Does anyone have any ideas as to what I can do to get video streaming working via node.js?

Thanks!
Chris


Solution 1:

I know this is a really old question, but as Google seems to like it I thought it would be worth pointing out that I wrote a Node.js video streaming module (Github, or via NPM) that's hopefully worth a look too.

Solution 2:

I was able to get this to work with some help from the nodejs forums:

http://groups.google.com/group/nodejs/browse_thread/thread/8339e0dc825c057f/822b2dd48f36e890

Highlights from the Google Groups thread:

Google chrome is known to first make a request with the range 0-1024 and then request the range "1024-".

response.end(file.slice(start, chunksize), "binary");

Then:

I was able to get the video to play no problems in firefox by setting the "connection" header to "close"

Then:

Seems that you are incorrectly computing the content-length:

var chunksize = (end-start)+1;

If start is 0 and end is 1, in your case chunksize is 2, and it should be 1.

Solution 3:

This solution does an asynchronous read of a server side video or audio media file ... it spins up a nodejs server at URL visible at

http://localhost:8888/

also it correctly handles client side HTML5 (browser/app) forward/back UI widget slider movements

save below code snippet as server side file :

media_server.js

... execute it on server side using

node media_server.js

enjoy

var http = require('http'),
    fs = require('fs'),
    util = require('util');

var path = "/path/to/local/video/or/audio/file/on/server.mp4";

var port = 8888;
var host = "localhost";
 
http.createServer(function (req, res) {

  var stat = fs.statSync(path);
  var total = stat.size;

  if (req.headers.range) { // meaning client (browser) has moved the forward/back slider
                           // which has sent this request back to this server logic ... cool
    var range = req.headers.range;
    var parts = range.replace(/bytes=/, "").split("-");
    var partialstart = parts[0];
    var partialend = parts[1];
 
    var start = parseInt(partialstart, 10);
    var end = partialend ? parseInt(partialend, 10) : total-1;
    var chunksize = (end-start)+1;
    console.log('RANGE: ' + start + ' - ' + end + ' = ' + chunksize);
 
    var file = fs.createReadStream(path, {start: start, end: end});
    res.writeHead(206, { 'Content-Range': 'bytes ' + start + '-' + end + '/' + total, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4' });
    file.pipe(res);

  } else {

    console.log('ALL: ' + total);
    res.writeHead(200, { 'Content-Length': total, 'Content-Type': 'video/mp4' });
    fs.createReadStream(path).pipe(res);
  }
}).listen(port, host);

console.log("Server running at http://" + host + ":" + port + "/");

Solution 4:

Based on Sam9291's answer, I rewrote the function using createReadStream() and fixing some problems:

/**
 * Sends a static file to the HTTP client, supporting partial transfers.
 * 
 * @req HTTP request object
 * @res HTTP response object
 * @fn Path to file that should be sent
 * @contentType MIME type for the response (defaults to HTML)
 */      
function sendFile(req, res, fn, contentType) {

  contentType = contentType || "text/html";

  fs.stat(fn, function(err, stats) {
    var headers;

    if (err) {
      res.writeHead(404, {"Content-Type":"text/plain"});
      res.end("Could not read file");
      return;
    }

    var range = req.headers.range || "";    
    var total = stats.size;

    if (range) {

      var parts = range.replace(/bytes=/, "").split("-");
      var partialstart = parts[0];
      var partialend = parts[1];

      var start = parseInt(partialstart, 10);
      var end = partialend ? parseInt(partialend, 10) : total-1;

      var chunksize = (end-start)+1;

      headers = { 
        "Content-Range": "bytes " + start + "-" + end + "/" + total, 
        "Accept-Ranges": "bytes", 
        "Content-Length": chunksize, 
        "Content-Type": contentType 
      };
      res.writeHead(206, headers);

    } else {

      headers = { 
        "Accept-Ranges": "bytes", 
        "Content-Length": stats.size, 
        "Content-Type": contentType 
      };
      res.writeHead(200, headers);

    }

    var readStream = fs.createReadStream(fn, {start:start, end:end});
    readStream.pipe(res);    

  });

}