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