diff --git a/docs/api.md b/docs/api.md index 52d11252..b352ef7e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -15,20 +15,18 @@ npm install webtorrent ## Quick Example -```js +```html + + + ``` # WebTorrent API @@ -150,6 +148,33 @@ buf.name = 'Some file name' client.seed(buf, cb) ``` +## `client.render([opts], function onrendered (err) {})` + +Render files into the DOM using data attributes. + +```html + + + +``` + +The torrentId passed to `[data-torrent-src]` will use an existing matching torrent with client.get(), or it will create a new one with client.add(). If there's an existing matching torrent, then it won't be modified by a new magnet link, so no new webseeds, trackers, or other options will be added. + +If `opts` is specified, then the default options (shown below) will be overridden. + +```js +{ + elements: [Node], // An array of DOM nodes to render to (default=document.querySelectorAll('[data-torrent-src]')) + timeout: Number // Number of milliseconds to wait for downloads to initiate before using fallback url (default=5000ms) +} +``` + +If the `onrendered` callback is specified, then it will be called after all the DOM nodes are successfully rendered to. + +If no path is specified via `[data-torrent-path]`, then `torrent.files[0]` will be used. + +The path in `[data-torrent-path]` is relative to the torrent's root folder, unlike `file.path` which includes the root directory. + ## `client.on('torrent', function (torrent) {})` Emitted when a torrent is ready to be used (i.e. metadata is available and store is diff --git a/docs/faq.md b/docs/faq.md index f8288ee8..f2e14324 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -224,18 +224,18 @@ script on your page. If you use [browserify](http://browserify.org/), you can It's easy to download a torrent and add it to the page. -```js +```html + + + ``` This supports video, audio, images, PDFs, Markdown, [and more][render-media], right diff --git a/index.js b/index.js index b90fc87d..51eaac0a 100644 --- a/index.js +++ b/index.js @@ -441,6 +441,90 @@ WebTorrent.prototype._destroy = function (err, cb) { self.dht = null } +/** + * Render files from a torrent into DOM nodes with custom attributes: + * [data-torrent-src] TorrentId to render + * [data-torrent-path] path to the file to render in a multi file torrent + * [data-torrent-fallback] fallback url for src + * @param {Object} opts + * @param {function} cb + */ +WebTorrent.prototype.render = function (opts, cb) { + var self = this + if (typeof opts === 'function') return self.render(null, opts) + + if (!opts) opts = {} + + var nodes = opts.elements || document.querySelectorAll('[data-torrent-src]') + + if (nodes.length === 0) return cb() + + var numRendered = 0 + + nodes.forEach(function (elem) { + var torrentId = elem.getAttribute('data-torrent-src') + + var torrent = self.get(torrentId) || self.add(torrentId) + + // don't pass error to fallback() because the cb() might have already been called before the torrent error + torrent.once('error', function () { fallback() }) + + // if nothing has been downloaded after timeout length, use fallback + setTimeout(function () { + if (torrent.downloaded === 0) { + fallback(new Error('Torrent with infohash: ' + torrent.infoHash + ' timed out. Using fallback url.')) + } + }, opts.timeout || 5000) + + // ensure metadata is available before using files in renderFile() + if (torrent.metadata) { + renderFile() + } else { + torrent.once('metadata', function () { renderFile() }) + } + + function renderFile () { + var filePath = elem.getAttribute('data-torrent-path') + + var fileToRender = torrent.files.find(function (file) { + if (!filePath) return true // if no path is specified, render the first file + + var rootDir = /\//.test(file.path) ? (torrent.name + '/') : '' + + // remove initial / if present + filePath = filePath.replace(/^\//, '') + + return file.path === rootDir + filePath + }) + + if (!fileToRender) { + return fallback(new Error('No file found matching this path: ' + filePath + ' in torrent with infohash: ' + torrent.infoHash)) + } + + // some errors from render-media are thrown, and some are passed to the callback + try { + fileToRender.renderTo(elem, { + autoplay: elem.hasAttribute('autoplay'), + controls: elem.hasAttribute('controls') + }, function (err) { + if (err) return fallback(err) + + if (++numRendered === nodes.length) cb() + }) + } catch (err) { + fallback(err) + } + } + + function fallback (error) { + var fallbackSrc = elem.getAttribute('data-torrent-fallback') + if (fallbackSrc) elem.setAttribute('src', fallbackSrc) + + if (error) cb(error) + } + }) +} + WebTorrent.prototype._onListening = function () { this._debug('listening') this.listening = true diff --git a/test/browser/basic.js b/test/browser/basic.js index 7bc488e3..256cfae7 100644 --- a/test/browser/basic.js +++ b/test/browser/basic.js @@ -96,6 +96,150 @@ if (!(global && global.process && global.process.versions && global.process.vers }) }) }) + + test('client.render() without argument', function (t) { + t.plan(11) + + var client = new WebTorrent({ dht: false, tracker: false }) + + client.on('error', function (err) { t.fail(err) }) + client.on('warning', function (err) { t.fail(err) }) + + client.seed(img, function (torrent) { + var tag = document.createElement('img') + tag.setAttribute('data-torrent-src', torrent.infoHash) + document.body.appendChild(tag) + + var tag2 = document.createElement('img') + tag2.setAttribute('data-torrent-src', torrent.infoHash) + document.body.appendChild(tag2) + + client.render(function (err) { + verifyImage(t, err, tag) + verifyImage(t, err, tag2) + client.destroy(function (err) { + t.error(err, 'client destroyed') + }) + }) + }) + }) + + test('client.render() with element argument', function (t) { + t.plan(7) + + var client = new WebTorrent({ dht: false, tracker: false }) + + client.on('error', function (err) { t.fail(err) }) + client.on('warning', function (err) { t.fail(err) }) + + client.seed(img, function (torrent) { + var elemToRender = document.createElement('img') + elemToRender.setAttribute('data-torrent-src', torrent.infoHash) + document.body.appendChild(elemToRender) + + var elem2 = document.createElement('img') + elem2.setAttribute('data-torrent-src', torrent.infoHash) + document.body.appendChild(elem2) + + client.render({elements: [elemToRender]}, function (err) { + verifyImage(t, err, elemToRender) + t.false(elem2.hasAttribute('src'), 'unspecified element should not be altered') + elem2.remove() + client.destroy(function (err) { + t.error(err, 'client destroyed') + }) + }) + }) + }) + + test('client.render() with path', function (t) { + t.plan(6) + + var client = new WebTorrent({ dht: false, tracker: false }) + + client.on('error', function (err) { t.fail(err) }) + client.on('warning', function (err) { t.fail(err) }) + + var text = Buffer.from('text') + text.name = 'text.txt' + + client.seed([text, img], function (torrent) { + var imgElem = document.createElement('img') + imgElem.setAttribute('data-torrent-src', torrent.infoHash) + imgElem.setAttribute('data-torrent-path', 'img.png') + document.body.appendChild(imgElem) + + var textElem = document.createElement('iframe') + textElem.setAttribute('data-torrent-src', torrent.infoHash) + textElem.setAttribute('data-torrent-path', 'text.txt') + document.body.appendChild(textElem) + + client.render(function (err) { + // error will be returned if either is rendered to the wrong element + verifyImage(t, err, imgElem) + textElem.remove() + client.destroy(function (err) { + t.error(err, 'client destroyed') + }) + }) + }) + }) + + test('client.render() fallback on render error', function (t) { + t.plan(3) + + var client = new WebTorrent({ dht: false, tracker: false }) + + client.on('error', function (err) { t.fail(err) }) + client.on('warning', function (err) { t.fail(err) }) + + client.seed(img, function (torrent) { + // use wrong element type to cause error in renderTo() + var elem = document.createElement('video') + elem.setAttribute('data-torrent-src', torrent.infoHash) + elem.setAttribute('data-torrent-fallback', 'fake url') + document.body.appendChild(elem) + + client.render(function (err) { + t.ok(err) + t.equals(elem.getAttribute('src'), 'fake url') + elem.remove() + client.destroy(function (err) { + t.error(err, 'client destroyed') + }) + }) + }) + }) + + test('client.render() fallback on torrent error', function (t) { + t.plan(3) + + var client = new WebTorrent({ dht: false, tracker: false }) + + client.on('error', function (err) { t.fail(err) }) + client.on('warning', function (err) { t.fail(err) }) + + client.seed(img, function (torrent) { + var elem = document.createElement('img') + elem.setAttribute('data-torrent-src', torrent.infoHash) + elem.setAttribute('data-torrent-fallback', 'fake url') + document.body.appendChild(elem) + + client.render(function (err) { + t.error(err) + process.nextTick(function () { + torrent.emit('error') + process.nextTick(function () { + t.equals(elem.getAttribute('src'), 'fake url') + elem.remove() + client.destroy(function (err) { + t.error(err, 'client destroyed') + }) + }) + }) + }) + }) + }) } test('WebTorrent.WEBRTC_SUPPORT', function (t) {