46

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

Tobu
  • 24,771
  • 4
  • 91
  • 98
Chris Harrington
  • 1,593
  • 3
  • 17
  • 26

8 Answers8

25

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.

meloncholy
  • 2,122
  • 18
  • 16
  • 6
    I love that kind of answers! Thank you. :) – Vasyl Boroviak Jun 06 '13 at 02:02
  • So coming back to that topic after 2 years... :) Is there a way of using that script to send live data (to the browser) that has been received by udp? – randomuser1 Apr 29 '15 at 12:58
  • @randomuser1 No, I'm afraid not. Live streaming is [more complicated](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/HTTPStreamingArchitecture/HTTPStreamingArchitecture.html#//apple_ref/doc/uid/TP40008332-CH101-SW2) (e.g. needs to segment the input, support index files) that the script doesn't do. I'd like to support this, but haven't had the time to work on it unfortunately. Sorry. – meloncholy Apr 29 '15 at 18:56
  • 1
    hey @meloncholy, actually I did it all already - I 'segmented' the input, put some indexes and I'm sending it through UDP. I can read them now in Node.js and I see them - number of each segment and its content. But I see it only in console and I want to merge it on the other site in the users browser - I don't know if that's possible – randomuser1 Apr 29 '15 at 19:03
19

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.

dab
  • 817
  • 1
  • 12
  • 23
Chris Harrington
  • 1,593
  • 3
  • 17
  • 26
  • I believe the chunksize was correct. According to https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html: `The first-byte-pos value in a byte-range-spec gives the byte-offset of the first byte in a range. The last-byte-pos value gives the byte-offset of the last byte in the range; that is, the byte positions specified are inclusive. Byte offsets start at zero.` – sthomps Mar 17 '17 at 18:28
14

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 + "/");
Scott Stensland
  • 26,870
  • 12
  • 93
  • 104
6

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);    

  });

}
Udo G
  • 12,572
  • 13
  • 56
  • 89
4

I am using the MVC framework sails.js on top of Node.js and I managed to get it working fine with the following code:

/**
 * VideoController
 *
 * @module      :: Controller
 * @description :: Contains logic for handling requests.
 */

 var fs = require('fs');

module.exports = {

  /* e.g.
  sayHello: function (req, res) {
    res.send('hello world!');
  }
  */

  /**
   * /video/stream
   */ 
  stream: function (req,res) {

    // This will render the view: 
    // C:\Users\sam\Documents\Dev\Fun\mymoviebank/views/video/stream.ejs
    res.view();

  },

  play: function (req,res) {

    fs.readFile('/Users/sam/Videos/big_buck_bunny.mp4', function (err, data) {
      if (err) throw err;

      var range = req.headers.range;
        var total = data.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;

        res.writeHead(206, { "Content-Range": "bytes " + start + "-" + end + "/" + total, "Accept-Ranges": "bytes", "Content-Length": chunksize, "Content-Type": 'video/mp4' });
        res.end(data);

    });

  }

};

Hope this helps

Samuel Poirier
  • 1,240
  • 2
  • 15
  • 30
  • 1
    npm's fs.statSync(../file/path/...) takes care of the details. See https://gist.github.com/westonplatter/7559003. – westonplatter Nov 20 '13 at 07:17
  • 3
    This solution will not scale - it pulls the entire video file into memory to serve only a small piece of it. ``fs.createReadStream(theFile, { start : $START, end : #END })`` will let you pipe the stream to the response and not need to load the entire video file into memory (imagine if 1000 users do that at the same time). – Tim Boudreau Jan 06 '14 at 03:42
3

I found this solution which seems to be simpler and (unlike the checked answer) works for me. (I tried adapting the coffeescript solution at the end of that thread and it kind of worked once I dealt with the fact that the initial request (for "bytes=0-") blows it up.

http://elegantcode.com/2011/04/06/taking-baby-steps-with-node-js-pumping-data-between-streams/

My actual implementation:

function stream_response( res, file_path, content_type ){
    var readStream = fs.createReadStream(file_path);

    readStream.on('data', function(data) {
        var flushed = res.write(data);
        // Pause the read stream when the write stream gets saturated
        console.log( 'streaming data', file_path );
        if(!flushed){
            readStream.pause();
        }
    });

    res.on('drain', function() {
        // Resume the read stream when the write stream gets hungry 
        readStream.resume();
    });

    readStream.on('end', function() {
        res.end();
    });

    readStream.on('error', function(err) {
        console.error('Exception', err, 'while streaming', file_path);
        res.end();
    });

    res.writeHead(200, {'Content-Type': content_type});
}
podperson
  • 2,284
  • 2
  • 24
  • 24
  • This does stream media just fine ... yet it would need to process request.headers in order to react to client side widget requests like skipping ahead/behind on the source media ... nice job – Scott Stensland Feb 24 '15 at 18:29
0

when using express put this in your media_server.js or index.js which will serve the media on port 3000

const express = require('express')
const fs = require('fs')
const path = require('path')
const app = express()

app.use(express.static(path.join(__dirname, 'public')))

app.get('/', function(req, res) {
  res.sendFile(path.join(__dirname + '/index.html'))
})

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

app.listen(3000, function () {
  console.log('Listening on port 3000!')
})

then in your index.html

<html>
  <head>
    <title>Video stream sample</title>
  </head>
  <body>
    <video id="videoPlayer" controls muted="muted" autoplay> 
      <source src="http://localhost:3000/video" type="video/mp4">
    </video>
  </body>
</html>
Kevin Muchwat
  • 1,445
  • 18
  • 18
0

I found this codesandbox and it seems really helpful https://codesandbox.io/s/14n6q1yr33