├── .gitignore ├── package.json ├── README.md ├── client.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webtorrent-remote", 3 | "version": "2.1.0", 4 | "description": "Run WebTorrent in one process, control it from another process or even another machine", 5 | "scripts": { 6 | "test": "standard" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/dcposch/webtorrent-remote.git" 11 | }, 12 | "author": "", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/dcposch/webtorrent-remote/issues" 16 | }, 17 | "homepage": "https://github.com/dcposch/webtorrent-remote#readme", 18 | "devDependencies": { 19 | "standard": "*" 20 | }, 21 | "dependencies": { 22 | "debug": "^2.6.3", 23 | "throttleit": "^1.0.0", 24 | "webtorrent": "0.x" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webtorrent-remote [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [npm-image]: https://img.shields.io/npm/v/webtorrent-remote.svg 4 | [npm-url]: https://npmjs.org/package/webtorrent-remote 5 | [downloads-image]: https://img.shields.io/npm/dm/webtorrent-remote.svg 6 | [downloads-url]: https://npmjs.org/package/webtorrent-remote 7 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 8 | [standard-url]: https://standardjs.com 9 | 10 | run WebTorrent in one process, control it from another process or even another machine. 11 | 12 | plain Javascript, no es6 13 | 14 | ## server process 15 | 16 | ```js 17 | var WebTorrentRemoteServer = require('webtorrent-remote/server') 18 | 19 | var opts = null 20 | var server = new WebTorrentRemoteServer(send, opts) 21 | 22 | function send (message) { 23 | // Send `message` to the correct client. It's JSON serializable. 24 | // Use TCP, some kind of IPC, whatever. 25 | // If there are multiple clients, look at message.clientKey 26 | } 27 | 28 | // When messages come back from the IPC channel, call: 29 | server.receive(message) 30 | ``` 31 | 32 | ### server options 33 | 34 | #### `opts.heartbeatTimeout` 35 | 36 | remove clients if we don't hear a heartbeat for this many milliseconds. default 37 | 30000 (30 seconds). set to 0 to disable the heartbeat check. once a torrent has no 38 | remaining clients, it will be removed. once there are no remaining torrents, the 39 | whole webtorrent instance will be destroyed. the webtorrent instance is created 40 | lazily the first time a client calls `add()`. 41 | 42 | #### `opts.updateInterval` 43 | 44 | send progress updates every x milliseconds to all clients of all torrents. default 45 | 1000 (1 second). set to 0 to disable progress updates. 46 | 47 | #### other options 48 | 49 | all WebTorrent options. the options object is passed to the constructor for the 50 | underlying WebTorrent instance. 51 | 52 | #### debugging 53 | 54 | This package uses [`debug`](https://www.npmjs.com/package/debug) for debug logging. Set the environment variable `DEBUG=webtorrent-remote` for detailed debug logs. 55 | 56 | ## client process(es) 57 | 58 | ```js 59 | var WebTorrentRemoteClient = require('webtorrent-remote/client') 60 | 61 | var opts = null 62 | var client = new WebTorrentRemoteClient(send, opts) 63 | 64 | function send (message) { 65 | // Same as above, except send the message to the server process 66 | } 67 | 68 | // When messages come back from the server, call: 69 | client.receive(message) 70 | 71 | // Now `client` is a drop-in replacement for the normal WebTorrent object! 72 | var torrentId = 'magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d' 73 | client.add(torrentId, function (err, torrent) { 74 | torrent.on('metadata', function () { 75 | console.log(JSON.stringify(torrent.files)) 76 | // Prints [{name:'sintel.mp4'}] 77 | }) 78 | 79 | var server = torrent.createServer() 80 | server.listen(function () { 81 | console.log('http://localhost:' + server.address().port) 82 | // Paste that into your browser to stream Sintel! 83 | }) 84 | }) 85 | ``` 86 | 87 | ### client options 88 | 89 | #### `opts.heartbeat` 90 | 91 | send a heartbeat once every x milliseconds. default 5000 (5 seconds). set to 0 to 92 | disable heartbeats. 93 | 94 | ### client methods 95 | 96 | #### `client.add(torrentID, [options], callback)` 97 | 98 | like `WebTorrent.add`, but only async. calls back with `(err, torrent)`. The 99 | `torrent` is a torrent object (see below for methods). 100 | 101 | #### `client.get(torrentID, callback)` 102 | 103 | like `WebTorrent.get`, but async. calls back with `(err, torrent)`. if the 104 | torrentId is not yet in the client, `err.name` will be `'TorrentMissingError'`. 105 | 106 | #### `client.destroy()` 107 | 108 | like `WebTorrent.destroy`, but destroys only this client. if a given torrent has 109 | no clients left, it will be destroyed too. if all torrents are gone, the whole 110 | WebTorrent instance will be destroyed on the server side. 111 | 112 | ### client events, from webtorrent 113 | 114 | - `client.on('error', () => {...})` 115 | - `client.on('warning', () => {...})` 116 | 117 | ### torrent methods 118 | 119 | the client gives you a torrent object in the callback to `get` or `add`. this 120 | supports a subset of the WebTorrent API, forwarding commands to the 121 | WebTorrentRemoteServer and emitting events: 122 | 123 | #### `torrent.createServer()` 124 | 125 | create a local torrent-to-HTTP streaming server. 126 | 127 | ### torrent events, unique to webtorrent-remote, not in webtorrent 128 | 129 | - `torrent.on('update', () => {...})`: fires periodically, see `updateInterval` 130 | 131 | ### torrent events, from webtorrent 132 | 133 | - `torrent.on('infohash', () => {...})` 134 | - `torrent.on('metadata', () => {...})` 135 | - `torrent.on('download', () => {...})` 136 | - `torrent.on('upload', () => {...})` 137 | - `torrent.on('done', () => {...})` 138 | - `torrent.on('error', () => {...})` 139 | - `torrent.on('warning', () => {...})` 140 | 141 | ### torrent props unique to webtorrent-remote, not in webtorrent 142 | 143 | - `torrent.client`: the WebTorrentRemoteClient 144 | - `torrent.key`: the clientKey used for messaging 145 | 146 | ### torrent props, from webtorrent (updated once on `infohash` or `metadata`) 147 | 148 | - `torrent.infoHash` 149 | - `torrent.name` 150 | - `torrent.length` 151 | - `torrent.files` 152 | 153 | ### torrent props, from webtorrent (updated on every `progress` event) 154 | 155 | - `torrent.progress` 156 | - `torrent.downloaded` 157 | - `torrent.uploaded` 158 | - `torrent.downloadSpeed` 159 | - `torrent.uploadSpeed` 160 | - `torrent.numPeers` 161 | - `torrent.progress` 162 | - `torrent.timeRemaining` 163 | 164 | ### server methods 165 | 166 | #### `server.address()` 167 | 168 | gets an address object like `{ address: '::', family: 'IPv6', port: 52505 }` that 169 | shows what host and port the server is listening on. 170 | 171 | #### `server.listen(onlistening)` 172 | 173 | tells the server to start listening. the `onlistening` function is called when the server starts listening. 174 | 175 | ### server events 176 | 177 | - `server.on('listening', () => {...})` 178 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | module.exports = WebTorrentRemoteClient 2 | 3 | var EventEmitter = require('events') 4 | 5 | /** 6 | * Provides the WebTorrent API. 7 | * 8 | * Communicates with a `WebTorrentRemoteServer` instance in another process or even 9 | * another machine. 10 | * 11 | * Contains: 12 | * - a subset of the methods and props of the WebTorrent client object 13 | * - clientKey, the UUID that's included in all IPC messages to/from this client 14 | * - torrents, a map from torrent key (also a UUID) to torrent handle 15 | * 16 | * Constructor creates the client and introduces it to the server. 17 | * - send, should be a function (message) {...} that passes the message to 18 | * WebTorrentRemoteServer 19 | * - opts, optionally specifies {heartbeat}, the heartbeat interval in milliseconds 20 | */ 21 | 22 | function WebTorrentRemoteClient (send, opts) { 23 | EventEmitter.call(this) 24 | if (!opts) opts = {} 25 | 26 | this._send = send 27 | 28 | this.clientKey = generateUniqueKey() 29 | this.torrents = {} 30 | 31 | this._destroyed = false 32 | 33 | var heartbeat = opts.heartbeat != null ? opts.heartbeat : 5000 34 | if (heartbeat > 0) { 35 | this._interval = setInterval(sendHeartbeat.bind(null, this), heartbeat) 36 | } 37 | } 38 | 39 | WebTorrentRemoteClient.prototype = Object.create(EventEmitter.prototype) 40 | 41 | /** 42 | * Receives a message from the WebTorrentRemoteServer 43 | */ 44 | WebTorrentRemoteClient.prototype.receive = function (message) { 45 | if (message.clientKey !== this.clientKey) { 46 | return console.error('ignoring message, expected clientKey ' + this.clientKey + 47 | ': ' + JSON.stringify(message)) 48 | } 49 | if (this._destroyed) { 50 | return console.error('ignoring message, client is destroyed: ' + this.clientKey) 51 | } 52 | switch (message.type) { 53 | // Public events. These are part of the WebTorrent API 54 | case 'infohash': 55 | return handleInfo(this, message) 56 | case 'metadata': 57 | return handleInfo(this, message) 58 | case 'download': 59 | return handleInfo(this, message) 60 | case 'upload': 61 | return handleInfo(this, message) 62 | case 'update': 63 | return handleInfo(this, message) 64 | case 'done': 65 | return handleInfo(this, message) 66 | case 'error': 67 | return handleError(this, message) 68 | case 'warning': 69 | return handleError(this, message) 70 | 71 | // Internal events. Used to trigger callbacks, not part of the public event API 72 | case 'server-ready': 73 | return handleServerReady(this, message) 74 | case 'torrent-subscribed': 75 | return handleSubscribed(this, message) 76 | default: 77 | console.error('ignoring message, unknown type: ' + JSON.stringify(message)) 78 | } 79 | } 80 | 81 | // Gets an existing torrent. Returns a torrent handle. 82 | // Emits either the `torrent-present` or `torrent-absent` event on that handle. 83 | WebTorrentRemoteClient.prototype.get = function (torrentId, cb) { 84 | var torrentKey = generateUniqueKey() 85 | this._send({ 86 | type: 'subscribe', 87 | clientKey: this.clientKey, 88 | torrentKey: torrentKey, 89 | torrentId: torrentId 90 | }) 91 | subscribeTorrentKey(this, torrentKey, cb) 92 | } 93 | 94 | // Adds a new torrent. See [client.add](https://webtorrent.io/docs) 95 | // - torrentId is a magnet link, etc 96 | // - opts can contain {announce, path, ...} 97 | // All parameters should be JSON serializable. 98 | WebTorrentRemoteClient.prototype.add = function (torrentId, opts, cb) { 99 | if (typeof opts === 'function') return this.add(torrentId, null, opts) 100 | if (!opts) opts = {} 101 | 102 | var torrentKey = opts.torrentKey || generateUniqueKey() 103 | this._send({ 104 | type: 'add-torrent', 105 | clientKey: this.clientKey, 106 | torrentKey: torrentKey, 107 | torrentId: torrentId, 108 | opts: opts 109 | }) 110 | subscribeTorrentKey(this, torrentKey, cb) 111 | } 112 | 113 | // Destroys the client 114 | // If this was the last client for a given torrent, destroys that torrent too 115 | WebTorrentRemoteClient.prototype.destroy = function () { 116 | if (this._destroyed) return 117 | this._destroyed = true 118 | 119 | this._send({ 120 | type: 'destroy', 121 | clientKey: this.clientKey 122 | }) 123 | 124 | clearInterval(this._interval) 125 | this._interval = null 126 | this._send = null 127 | } 128 | 129 | /** 130 | * Refers to a WebTorrent torrent object that lives in a different process. 131 | * Contains: 132 | * - the same API (for now, just a subset) 133 | * - client, the underlying WebTorrentRemoteClient 134 | * - key, the UUID that uniquely identifies this torrent 135 | */ 136 | function RemoteTorrent (client, key) { 137 | EventEmitter.call(this) 138 | 139 | // New props unique to webtorrent-remote, not in webtorrent 140 | this.client = client 141 | this.key = key 142 | 143 | // WebTorrent API, props updated once: 144 | this.infoHash = null 145 | this.name = null 146 | this.length = null 147 | this.files = [] 148 | 149 | // WebTorrent API, props updated with every `progress` event: 150 | this.progress = 0 151 | this.downloaded = 0 152 | this.uploaded = 0 153 | this.downloadSpeed = 0 154 | this.uploadSpeed = 0 155 | this.numPeers = 0 156 | this.progress = 0 157 | this.timeRemaining = Infinity 158 | } 159 | 160 | RemoteTorrent.prototype = Object.create(EventEmitter.prototype) 161 | 162 | /* 163 | * Creates a streaming torrent-to-HTTP server 164 | * - opts can contain {headers, ...} 165 | */ 166 | RemoteTorrent.prototype.createServer = function (opts) { 167 | this.server = new RemoteTorrentServer(this, opts) 168 | return this.server 169 | } 170 | 171 | function RemoteTorrentServer (torrent, opts) { 172 | EventEmitter.call(this) 173 | 174 | this.torrent = torrent 175 | this._createServerOpts = opts 176 | this._addr = null 177 | } 178 | 179 | RemoteTorrentServer.prototype = Object.create(EventEmitter.prototype) 180 | 181 | RemoteTorrentServer.prototype.address = function (cb) { 182 | return this._addr 183 | } 184 | 185 | RemoteTorrentServer.prototype.listen = function (onlistening) { 186 | this.once('listening', onlistening) 187 | this.torrent.client._send({ 188 | type: 'create-server', 189 | clientKey: this.torrent.client.clientKey, 190 | torrentKey: this.torrent.key, 191 | opts: this._createServerOpts 192 | }) 193 | } 194 | 195 | function subscribeTorrentKey (client, torrentKey, cb) { 196 | var torrent = new RemoteTorrent(client, torrentKey) 197 | torrent._subscribedCallback = cb 198 | client.torrents[torrentKey] = torrent 199 | } 200 | 201 | function sendHeartbeat (client) { 202 | client._send({ 203 | type: 'heartbeat', 204 | clientKey: client.clientKey 205 | }) 206 | } 207 | 208 | function handleInfo (client, message) { 209 | var torrent = getTorrentByKey(client, message.torrentKey) 210 | Object.assign(torrent, message.torrent) 211 | torrent.emit(message.type) 212 | } 213 | 214 | function handleError (client, message) { 215 | var type = message.type // 'error' or 'warning' 216 | if (message.torrentKey) { 217 | var torrent = getTorrentByKey(client, message.torrentKey) 218 | if (torrent.listeners(type).length > 0) torrent.emit(type, message.error) 219 | else client.emit(type, message.error) 220 | } else { 221 | client.emit(type, message.error) 222 | } 223 | } 224 | 225 | function handleServerReady (client, message) { 226 | var torrent = getTorrentByKey(client, message.torrentKey) 227 | if (torrent.server) { 228 | torrent.server._addr = message.serverAddress 229 | torrent.server.emit('listening') 230 | } 231 | } 232 | 233 | function handleSubscribed (client, message) { 234 | var torrent = getTorrentByKey(client, message.torrentKey) 235 | var cb = torrent._subscribedCallback 236 | if (message.torrent) { 237 | Object.assign(torrent, message.torrent) // Fill in infohash, etc 238 | cb(null, torrent) 239 | } else { 240 | var err = new Error('Invalid torrent identifier') 241 | err.name = 'TorrentMissingError' 242 | delete client.torrents[message.torrentKey] 243 | cb(err) 244 | } 245 | } 246 | 247 | function getTorrentByKey (client, torrentKey) { 248 | var torrent = client.torrents[torrentKey] 249 | if (torrent) return torrent 250 | throw new Error('Unrecognized torrentKey: ' + torrentKey) 251 | } 252 | 253 | function generateUniqueKey () { 254 | return Math.random().toString(16).slice(2) 255 | } 256 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | module.exports = WebTorrentRemoteServer 2 | 3 | var debug = require('debug')('webtorrent-remote') 4 | var parseTorrent = require('parse-torrent') 5 | var throttle = require('throttleit') 6 | var WebTorrent = require('webtorrent') 7 | 8 | /** 9 | * Runs WebTorrent. 10 | * Connects to trackers, the DHT, BitTorrent peers, and WebTorrent peers. 11 | * Controlled by one or more WebTorrentRemoteClients. 12 | * - send is a function (message) { ... } 13 | * Must deliver them message to the WebTorrentRemoteClient 14 | * If there is more than one client, you must check message.clientKey 15 | * - opts is passed to the WebTorrent varructor 16 | */ 17 | function WebTorrentRemoteServer (send, opts) { 18 | this._send = send 19 | this._webtorrent = null 20 | this._clients = {} 21 | this._torrents = [] 22 | 23 | this._webtorrentOpts = opts 24 | 25 | this._heartbeatTimeout = opts.heartbeatTimeout != null 26 | ? opts.heartbeatTimeout 27 | : 30000 28 | 29 | var updateInterval = opts.updateInterval != null 30 | ? opts.updateInterval 31 | : 1000 32 | 33 | if (updateInterval) { 34 | setInterval(sendUpdates.bind(null, this), updateInterval) 35 | } 36 | } 37 | 38 | // Receives a message from the WebTorrentRemoteClient 39 | // Message contains {clientKey, type, ...} 40 | WebTorrentRemoteServer.prototype.receive = function (message) { 41 | var clientKey = message.clientKey 42 | if (!this._clients[clientKey]) { 43 | debug('adding client, clientKey: ' + clientKey) 44 | this._clients[clientKey] = { 45 | clientKey: clientKey, 46 | heartbeat: Date.now() 47 | } 48 | } 49 | switch (message.type) { 50 | case 'subscribe': 51 | return handleSubscribe(this, message) 52 | case 'add-torrent': 53 | return handleAddTorrent(this, message) 54 | case 'create-server': 55 | return handleCreateServer(this, message) 56 | case 'heartbeat': 57 | return handleHeartbeat(this, message) 58 | case 'destroy': 59 | return handleDestroy(this, message) 60 | default: 61 | console.error('ignoring unknown message type: ' + JSON.stringify(message)) 62 | } 63 | } 64 | 65 | // Returns the underlying WebTorrent object, lazily creating it if needed 66 | WebTorrentRemoteServer.prototype.webtorrent = function () { 67 | if (!this._webtorrent) { 68 | this._webtorrent = new WebTorrent(this._webtorrentOpts) 69 | addWebTorrentEvents(this) 70 | } 71 | return this._webtorrent 72 | } 73 | 74 | function send (server, message) { 75 | debug('sending %o', message) 76 | server._send(message) 77 | } 78 | 79 | // Event handlers for the whole WebTorrent instance 80 | function addWebTorrentEvents (server) { 81 | server._webtorrent.on('warning', function (e) { sendError(server, null, e, 'warning') }) 82 | server._webtorrent.on('error', function (e) { sendError(server, null, e, 'error') }) 83 | } 84 | 85 | // Event handlers for individual torrents 86 | function addTorrentEvents (server, torrent) { 87 | torrent.on('infohash', function () { sendInfo(server, torrent, 'infohash') }) 88 | torrent.on('metadata', function () { sendInfo(server, torrent, 'metadata') }) 89 | torrent.on('download', throttle(function () { sendProgress(server, torrent, 'download') }, 1000)) 90 | torrent.on('upload', throttle(function () { sendProgress(server, torrent, 'upload') }, 1000)) 91 | torrent.on('done', function () { sendProgress(server, torrent, 'done') }) 92 | torrent.on('warning', function (e) { sendError(server, torrent, e, 'warning') }) 93 | torrent.on('error', function (e) { sendError(server, torrent, e, 'error') }) 94 | } 95 | 96 | // Subscribe does NOT create a new torrent or join a new swarm 97 | // If message.torrentId is missing, it emits 'torrent-subscribed' with {torrent: null} 98 | // If the webtorrent instance hasn't been created at all yet, subscribe won't create it 99 | function handleSubscribe (server, message) { 100 | var wt = server._webtorrent // Don't create the webtorrent instance 101 | var clientKey = message.clientKey 102 | var torrentKey = message.torrentKey 103 | 104 | // See if this torrent is already added 105 | parseTorrent.remote(message.torrentId, function (err, parsedTorrent) { 106 | if (err) { 107 | sendSubscribed(server, null, clientKey, torrentKey) 108 | } else { 109 | var torrent = wt && wt.torrents.find(function (t) { 110 | return t.infoHash === parsedTorrent.infoHash 111 | }) 112 | 113 | // If so, listen for updates 114 | if (torrent) { 115 | torrent.clients.push({clientKey: clientKey, torrentKey: torrentKey}) 116 | } 117 | 118 | sendSubscribed(server, torrent, clientKey, torrentKey) 119 | } 120 | }) 121 | } 122 | 123 | // Emits the 'torrent-subscribed' event 124 | function sendSubscribed (server, torrent, clientKey, torrentKey) { 125 | var response = { 126 | type: 'torrent-subscribed', 127 | torrent: null, 128 | clientKey: clientKey, 129 | torrentKey: torrentKey 130 | } 131 | 132 | if (torrent) { 133 | response.torrent = Object.assign( 134 | getInfoMessage(server, torrent, '').torrent, 135 | getProgressMessage(server, torrent, '').torrent 136 | ) 137 | } 138 | 139 | send(server, response) 140 | } 141 | 142 | function handleAddTorrent (server, message) { 143 | var clientKey = message.clientKey 144 | var torrentKey = message.torrentKey 145 | 146 | // First, see if we've already joined this swarm 147 | parseTorrent.remote(message.torrentId, function (err, parsedTorrent) { 148 | if (err) { 149 | sendSubscribed(server, null, clientKey, torrentKey) 150 | } else { 151 | var infoHash = parsedTorrent.infoHash 152 | var torrent = server._torrents.find(function (t) { 153 | return t.infoHash === infoHash 154 | }) 155 | 156 | // If not, add the torrent to the client 157 | if (!torrent) { 158 | debug('add torrent: ' + infoHash + ' ' + (parsedTorrent.name || '')) 159 | torrent = server.webtorrent().add(message.torrentId, message.opts) 160 | torrent.clients = [] 161 | server._torrents.push(torrent) 162 | addTorrentEvents(server, torrent) 163 | } 164 | 165 | // Either way, subscribe this client to future updates for this swarm 166 | torrent.clients.push({ 167 | clientKey: clientKey, 168 | torrentKey: torrentKey 169 | }) 170 | 171 | sendSubscribed(server, torrent, clientKey, torrentKey) 172 | } 173 | }) 174 | } 175 | 176 | function handleCreateServer (server, message) { 177 | var clientKey = message.clientKey 178 | var torrentKey = message.torrentKey 179 | var opts = message.opts 180 | var torrent = getTorrentByKey(server, torrentKey) 181 | if (!torrent) return 182 | 183 | function done () { 184 | send(server, { 185 | clientKey: clientKey, 186 | torrentKey: torrentKey, 187 | serverAddress: torrent.serverAddress, 188 | type: 'server-ready' 189 | }) 190 | } 191 | 192 | if (torrent.serverAddress) { 193 | // Server already exists. Call back right away 194 | done() 195 | } else if (torrent.pendingServerCallbacks) { 196 | // Server pending 197 | // listen() has already been called, but the 'listening' event hasn't fired yet 198 | torrent.pendingServerCallbacks.push(done) 199 | } else { 200 | // Server does not yet exist. Create it, then notify everyone who asked for it 201 | torrent.pendingServerCallbacks = [done] 202 | torrent.server = torrent.createServer(opts) 203 | torrent.server.listen(undefined, 'localhost', undefined, function () { 204 | torrent.serverAddress = torrent.server.address() 205 | torrent.pendingServerCallbacks.forEach(function (cb) { cb() }) 206 | delete torrent.pendingServerCallbacks 207 | }) 208 | } 209 | } 210 | 211 | function handleHeartbeat (server, message) { 212 | var client = server._clients[message.clientKey] 213 | if (!client) return console.error('skipping heartbeat for unknown clientKey ' + message.clientKey) 214 | client.heartbeat = Date.now() 215 | } 216 | 217 | // Removes a client from all torrents 218 | // If the torrent has no clients left, destroys the torrent 219 | function handleDestroy (server, message) { 220 | var clientKey = message.clientKey 221 | killClient(server, clientKey) 222 | debug('destroying client ' + clientKey) 223 | } 224 | 225 | function sendInfo (server, torrent, type) { 226 | var message = getInfoMessage(server, torrent, type) 227 | sendToTorrentClients(server, torrent, message) 228 | } 229 | 230 | function sendProgress (server, torrent, type) { 231 | var message = getProgressMessage(server, torrent, type) 232 | sendToTorrentClients(server, torrent, message) 233 | } 234 | 235 | function getInfoMessage (server, torrent, type) { 236 | return { 237 | type: type, 238 | torrent: { 239 | name: torrent.name, 240 | infoHash: torrent.infoHash, 241 | length: torrent.length, 242 | files: (torrent.files || []).map(function (file) { 243 | return { 244 | name: file.name, 245 | length: file.length 246 | } 247 | }) 248 | } 249 | } 250 | } 251 | 252 | function getProgressMessage (server, torrent, type) { 253 | return { 254 | type: type, 255 | torrent: { 256 | progress: torrent.progress, 257 | downloaded: torrent.downloaded, 258 | uploaded: torrent.uploaded, 259 | length: torrent.length, 260 | downloadSpeed: torrent.downloadSpeed, 261 | uploadSpeed: torrent.uploadSpeed, 262 | ratio: torrent.ratio, 263 | numPeers: torrent.numPeers, 264 | timeRemaining: torrent.timeRemaining 265 | } 266 | } 267 | } 268 | 269 | function sendError (server, torrent, e, type) { 270 | var message = { 271 | type: type, // 'warning' or 'error' 272 | error: { 273 | message: e.message, 274 | stack: e.stack 275 | } 276 | } 277 | if (torrent) sendToTorrentClients(server, torrent, message) 278 | else sendToAllClients(server, message) 279 | } 280 | 281 | function sendUpdates (server) { 282 | if (server._heartbeatTimeout > 0) { 283 | removeDeadClients(server, server._heartbeatTimeout) 284 | } 285 | server._torrents.forEach(function (torrent) { 286 | sendProgress(server, torrent, 'update') 287 | }) 288 | } 289 | 290 | function removeDeadClients (server, heartbeatTimeout) { 291 | var now = Date.now() 292 | for (var clientKey in server._clients) { 293 | var client = server._clients[clientKey] 294 | if (now - client.heartbeat <= heartbeatTimeout) continue 295 | killClient(server, clientKey) 296 | debug('torrent client died, clientKey: ' + clientKey) 297 | } 298 | } 299 | 300 | function killClient (server, clientKey) { 301 | // Remove client from server 302 | delete server._clients[clientKey] 303 | 304 | // Remove clients from torrents 305 | server._torrents.forEach(function (torrent) { 306 | torrent.clients = torrent.clients.filter(function (c) { 307 | return c.clientKey !== clientKey 308 | }) 309 | 310 | if (torrent.clients.length === 0) { 311 | debug('torrent has no clients left, destroy after 10s: ' + torrent.name) 312 | setTimeout(destroyTorrent, 10000) 313 | } 314 | 315 | function destroyTorrent () { 316 | if (torrent.clients.length > 0) { 317 | return debug('torrent has new clients, skipping destroy') 318 | } 319 | debug('torrent destroyed, all clients died: ' + torrent.name) 320 | torrent.destroy() 321 | 322 | // Remove destroyed torrents from server 323 | server._torrents = server._torrents.filter(function (t) { 324 | return !t.destroyed 325 | }) 326 | 327 | // If the last torrent is gone, kill the whole WebTorrent instance 328 | if (server._webtorrent && server._torrents.length === 0) { 329 | server._webtorrent.destroy() 330 | server._webtorrent = null 331 | debug('webtorrent destroyed, no torrents left') 332 | } 333 | } 334 | }) 335 | } 336 | 337 | function sendToTorrentClients (server, torrent, message) { 338 | torrent.clients.forEach(function (client) { 339 | var clientMessage = Object.assign({}, message, client) 340 | send(server, clientMessage) 341 | }) 342 | } 343 | 344 | function sendToAllClients (server, message) { 345 | for (var clientKey in server._clients) { 346 | var clientMessage = Object.assign({}, message, {clientKey: clientKey}) 347 | send(server, clientMessage) 348 | } 349 | } 350 | 351 | function getTorrentByKey (server, torrentKey) { 352 | var torrent = server.webtorrent().torrents.find(function (t) { return hasTorrentKey(t, torrentKey) }) 353 | if (!torrent) { 354 | var message = 'missing torrentKey: ' + torrentKey 355 | sendError(server, null, {message: message}, 'warning') 356 | } 357 | return torrent 358 | } 359 | 360 | // Each torrent corresponds to *one or more* torrentKeys That's because clients 361 | // generate torrentKeys independently, and we might have two clients that both 362 | // added a torrent with the same infoHash. (In that case, two RemoteTorrent objects 363 | // correspond to the same WebTorrent torrent object.) 364 | function hasTorrentKey (torrent, torrentKey) { 365 | return torrent.clients.some(function (c) { return c.torrentKey === torrentKey }) 366 | } 367 | --------------------------------------------------------------------------------