pbie42
pbie42

Reputation: 625

How to stream currently downloading video to html5 player with node.js?

I am currently working on a project that downloads a torrent and, while it is downloading, streams it to an html5 video player. The constraints of my project do not let me use peerflix or webtorrent. I am currently using the torrent-stream module. My code is in node.js and pug. It is as follows:

server.js

var uri = 'magnet:?xt=urn:btih:11a2ac68a11634e980f265cb1433c599d017a759';

var engine = torrentStream(uri);    

let engineGo = function () {
      return new Promise(function (resolve, reject) {
        var engine = torrentStream(uri);
        engine.on('ready', function() {
            engine.files.forEach(function(file) {
              if (file.name.substr(file.name.length - 3) == 'mkv' || file.name.substr(file.name.length - 3) == 'mp4') {
                console.log('filename:', file.name);
                var stream = file.createReadStream();
                var writable = fs.createWriteStream(file.name);
                stream.pipe(writable);
                engine.on('download', function () {
                  console.log(file.name);
                  console.log(engine.swarm.downloaded / file.length * 100 + "%");
                  resolve(file);
                });
              }
           });
       });
     });
}

app.get('*', function (req, res) {
    if (req.url != "/Guardians.of.the.Galaxy.2014.1080p.BluRay.x264.YIFY.mp4") {
      var rpath = __dirname + '/views/index.pug';
      fs.readFile(rpath, 'utf8', function (err, str) {
        var fn = pug.compile(str, { filename: rpath, pretty: true});
        res.writeHead(200, { "Content-Type": "text/html" });
        res.write(fn());
        res.end();
      });
    } else {
      engineGo().then(function (result) {
        var filer = path.resolve(__dirname,result.name);
        fs.stat(filer, function(err, stats) {
          if (err) {
            if (err.code === 'ENOENT') {
              // 404 Error if file not found
              return res.sendStatus(404);
            }
          res.end(err);
          }
          var range = req.headers.range;
          if (!range) {
           // 416 Wrong range
           return res.sendStatus(416);
          }

          var positions = range.replace(/bytes=/, "").split("-");
          var start = parseInt(positions[0], 10);
          var total = stats.size;
          var end = positions[1] ? parseInt(positions[1], 10) : total - 1;
          var chunksize = (end - start) + 1;

          res.writeHead(206, {
            "Content-Range": "bytes " + start + "-" + end + "/" + total,
            "Accept-Ranges": "bytes",
            'Connection': 'keep-alive',
            "Content-Length": chunksize,
            "Content-Type": "video/mp4"
          });
          var stream = fs.createReadStream(filer, { start: start, end: end })
          .on("open", function() {
             stream.pipe(res);
          }).on("data", function (data) {
             console.log(data);
          }).on("error", function(err) {
             res.end(err);
          });
        });
      });
    }
});

index.pug

doctype
html
  title
    |Welcome
  body
      video(id='video' src="http://localhost:8888/Guardians.of.the.Galaxy.2014.1080p.BluRay.x264.YIFY.mp4" type="video/mp4" controls)

At the moment this begins to work. Once I load the page the promise will execute and begin to download the torrent. Once the first bit of data from the torrent is sent through the stream on the .on('open') the video will begin to play. The player shows that more information is being added to the video as more is downloaded but it will never play past the initial data that was sent. Is there some way to tell the player that more data is coming and not to cut out the second it is done reading it's initial chunk that was sent? Any advice is helpful and thank you in advance!

Upvotes: 2

Views: 2905

Answers (1)

idbehold
idbehold

Reputation: 17168

You can try this out:

const path = require('path');
const parseRange = require('range-parser');
const engine = torrentStream('magnet:?xt=urn:btih:11a2ac68a11634e980f265cb1433c599d017a759');
const getTorrentFile = new Promise(function (resolve, reject) {
  engine.on('ready', function() {
    engine.files.forEach(function (file, idx) {
      const ext = path.extname(file.name).slice(1);
      if (ext === 'mkv' || ext === 'mp4') {
        file.ext = ext;
        resolve(file);
      }
    });
  });
});

app.use('*', function (req, res) {
  if (req.url != '/Guardians.of.the.Galaxy.2014.1080p.BluRay.x264.YIFY.mp4') {
    res.setHeader('Content-Type', 'text/html');
    if (req.method !== 'GET') return res.end();
    var rpath = __dirname + '/views/index.pug';
    fs.readFile(rpath, 'utf8', function (err, str) {
      var fn = pug.compile(str, { filename: rpath, pretty: true});
      res.end(fn());
    });
  } else {
    res.setHeader('Accept-Ranges', 'bytes');
    getTorrentFile.then(function (file) {
      res.setHeader('Content-Length', file.length);
      res.setHeader('Content-Type', `video/${file.ext}`);
      const ranges = parseRange(file.length, req.headers.range, { combine: true });
      if (ranges === -1) {
        // 416 Requested Range Not Satisfiable
        res.statusCode = 416;
        return res.end();
      } else if (ranges === -2 || ranges.type !== 'bytes' || ranges.length > 1) {
        // 200 OK requested range malformed or multiple ranges requested, stream entire video
        if (req.method !== 'GET') return res.end();
        return file.createReadStream().pipe(res);
      } else {
        // 206 Partial Content valid range requested
        const range = ranges[0];
        res.statusCode = 206;
        res.setHeader('Content-Length', 1 + range.end - range.start);
        res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${file.length}`);
        if (req.method !== 'GET') return res.end();
        return file.createReadStream(range).pipe(res);
      }
    }).catch(function (e) {
      console.error(e);
      res.end(e);
    });
  }
});

I'm using range-parser to get the requested byte range because it's throughly tested and handles odd edge cases. I'm also starting the torrent download immediately when the server starts up. This is because browsers will often make multiple requests for the same audio/video file just with different byte-ranges so starting a new torrent stream on each request for that file isn't very efficient.

Upvotes: 1

Related Questions