Torrent Streaming service with WebTorrent and Express

This tutorial is meant for people who already have an basic knowledge of programming, and an intermediate knowledge of Windows or Ubuntu. This tutorial is written mainly for Windows, but provides alternatives for Linux where applicable.

Step 1: Install nodejs and npm

To install Node and npm on Windows, you should go and download the Windows Installer from here. Run the Installer you've downloaded and follow the instructions on screen.

To verify you have installed Node, open CMD and run "node -v" and "npm -v". You should see something like this:

C:\Users\Milan>node -v
v5.4.0
C:\Users\Milan>npm -v
3.3.12

To install Node on Ubuntu or Debian, type in "sudo apt-get install nodejs npm" (ommit sudo for Debian, run as root).

Step 3: Initialize a project

Create a folder somewhere, I usually have a dedicated folder for my Projects, and name it whatever you want, for example "BrowserTorrent".

Then, create a file named "package.json" in it and put in the following contents:

{
    "name": "BrowserTorrent",
    "version": "1.0.0",
    "scripts": {
            "start": "node ./index.js"
    },
    "dependencies": {
        "express": "~4.13.1",
        "webtorrent": "~0.62.3"
    }
}

This file will tell Node what's our project name, what dependencies does our project require, how to start it, etc. To learn more about the package.json format, go here.

Now, go into the folder that you've made, and run "npm install". You should see something like this:

npm install result

Step 3: The HTML application

Make a folder called "app" in the BrowserTorrent folder. Inside of it, create a new file, named index.html, and paste the following contents:

<!DOCTYPE html>
<html>
<head>
    <title>BrowserTorrent</title>
    <meta charset="utf-8" />
    <script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <script>
    $(function() {
        var api = 'http://localhost:9111/';
        $('.stream').click(function() {
            window.infoHash = $('input').val().split('btih:')[1].split('&')[0];
            $.get(api + 'api/add/' + infoHash, function(data) {
                var video = api + 'stream/' + infoHash + '.mp4';
                $('video').attr('src', video);
                $('.input').hide();
                $('.video').show();
            });
        });
        $('.stop').click(function() {
            $.get(api + 'api/delete/' + infoHash, function(data) {
                $('video').attr('src', '');
                $('input').val('');
                window.infoHash = '';
                $('.input').show();
                $('.video').hide();
            });
        });
    });
    </script>
</head>
<body>
    <div class="container">
        <h1>BrowserTorrent</h1>
        <div class="video" style="display:none">
            <video width="640" height="360" autoplay preload controls type="video/mp4" src="" style="background:black"></video>
            <br>
            <button class="stop">Stop</button>
        </div>
        <div class="input">
            <p>Sample torrent: magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d&amp;dn=sintel.mp4 (Sintel Open Source Movie, H.264 MP4, ~120 MiB). </p>
            <input type="text" style="width:350px" name="torrent" placeholder="Enter torrent URL. " />
            <button class="stream">Stream!</button>
        </div>
    </div>
</body>
</html>

This file will be our Web User Interface, with which our user will interact. For the purpose of this demo, it's a very simple HTML file with JavaScript, CSS and HTML all mashed together, but that's just for simplicity's sake.

In a "real" application you would of course separate everything into different files and format it better.

Step 4: The Node Application

In the BrowserTorrent folder, create a new file, named "index.js". Note the JS at the end, this is different from HTML in the 3rd step. This will be our node application that acts as a web server and the torrent streaming API. Inside, paste the following content:

var express = require('express');
var webtorrent = require('webtorrent');
var path = require('path');
var http = require('http');
var app = express();

var port = 9111;

var client = new webtorrent();

// Allow Cross-Origin requests
app.use(function(req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'OPTIONS, POST, GET, PUT, DELETE');
    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
    next();
});

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

var getLargestFile = function (torrent) {
    var file;
    for(i = 0; i < torrent.files.length; i++) {
        if (!file || file.length < torrent.files[i].length) {
            file = torrent.files[i];
        }
    }
    return file;
};

var buildMagnetURI = function(infoHash) {
    return 'magnet:?xt=urn:btih:' + infoHash + '&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.istole.it%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969';
};

app.get('/api/add/:infoHash', function(req, res) {
    if(typeof req.params.infoHash == 'undefined' || req.params.infoHash == '') {
        res.status(500).send('Missing infoHash parameter!'); return;
    }
    var torrent = buildMagnetURI(req.params.infoHash);
    try {
        client.add(torrent, function (torrent) {
            var file = getLargestFile(torrent);
            torrent.swarm.on('upload', function() {
                if(torrent.length == torrent.downloaded) {
                    torrent.swarm.destroy();
                    torrent.discovery.stop();
                }
            });
            res.status(200).send('Added torrent!');
        });
    } catch (err) {
        res.status(500).send('Error: ' + err.toString());
    }
});


app.get('/stream/:infoHash.mp4', function(req, res, next) {
    if(typeof req.params.infoHash == 'undefined' || req.params.infoHash == '') {
        res.status(500).send('Missing infoHash parameter!'); return;
    }
    var torrent = buildMagnetURI(req.params.infoHash);
    try {
        var torrent = client.get(torrent);
        var file = getLargestFile(torrent);
        var total = file.length;

        if(typeof req.headers.range != 'undefined') {
            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;
        } else {
            var start = 0; var end = total;
        }

        var stream = file.createReadStream({start: start, end: end});
        res.writeHead(206, { 'Content-Range': 'bytes ' + start + '-' + end + '/' + total, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4' });
        stream.pipe(res);
    } catch (err) {
        res.status(500).send('Error: ' + err.toString());
    }
});


app.get('/api/delete/:infoHash', function(req, res, next) {
    if(typeof req.params.infoHash == 'undefined' || req.params.infoHash == '') {
        res.status(500).send('Missing infoHash parameter!'); return;
    }
    var torrent = buildMagnetURI(req.params.infoHash);
    try {
        var torrent = client.remove(torrent);
        res.status(200).send('Removed torrent. ');
    } catch (err) {
        res.status(500).send('Error: ' + err.toString());
    }
});

var server = http.createServer(app);
server.listen(port, function() {
    console.log('Listening on http://127.0.0.1:' + port);
});

Now, we're almost there. Save the file and move on to the next step.

Step 5: The demo

I have made a quick demo that showcases the application. It's embedded below:

Now, to try it for yourself on your PC, open cmd or any shell, and navigate to the BrowserPopcorn directory. Inside, run "npm start".

It should write out something like this:

C:\Users\Milan\Documents\Projects\BrowserTorrent>npm start

> BrowserTorrent@1.0.0 start C:\Users\Milan\Documents\Projects\BrowserTorrent
> node ./index.js

Listening on http://127.0.0.1:9111

Now, open your browser to the URL in the command line window, most probably http://127.0.0.1:9111/.

Copy the magnet link of Sintel, the open source movie, and paste it into the text field and press Start!

Be patient, it might take a few seconds for the stream to start, but soon, a video player will appear and Sintel will start playing!

Congratulations! You've made your very own torrent streaming engine for the browser. You can design it nicer, add some more features, package it up and install it on your server, and offer your own BrowserTorrent in the cloud!

If you wish to download the full source code of this project, you can do so here.

Thanks a lot for reading this tutorial. If you encounter any issues, don't hesitate to comment or contact me.