Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

Commit

Permalink
docs: Add browser example for ReadableStreams
Browse files Browse the repository at this point in the history
feat: Allows for byte offsets when using ipfs.files.cat and friends to request slices of files
  • Loading branch information
achingbrain committed Mar 27, 2018
1 parent 8e3ea44 commit b76eb50
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 6 deletions.
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Let us know if you find any issue or if you want to contribute and add a new tut
- [js-ipfs in the browser with a `<script>` tag](./browser-script-tag)
- [js-ipfs in electron](./run-in-electron)
- [Using streams to add a directory of files to ipfs](./browser-add-readable-stream)
- [Streaming video from ipfs to the browser using `ReadableStream`s](./browser-readablestream)

## Understanding the IPFS Stack

Expand Down
16 changes: 16 additions & 0 deletions examples/browser-readablestream/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Streaming video from IPFS using ReadableStreams

We can use the execllent [`videostream`](https://www.npmjs.com/package/videostream) to stream video from IPFS to the browser. All we need to do is return a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)-like object that contains the requested byte ranges.

Take a look at [`index.js`](./index.js) to see a working example.

## Running the demo

In this directory:

```
$ npm install
$ npm start
```

Then open [http://localhost:8888](http://localhost:8888) in your browser.
62 changes: 62 additions & 0 deletions examples/browser-readablestream/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title><%= htmlWebpackPlugin.options.title %></title>
<style type="text/css">

body {
margin: 0;
padding: 0;
}

#container {
display: flex;
height: 100vh;
}

pre {
flex-grow: 2;
padding: 10px;
height: calc(100vh - 45px);
overflow: auto;
}

#form-wrapper {
padding: 20px;
}

form {
padding-bottom: 10px;
display: flex;
}

#hash {
display: inline-block;
margin: 0 10px 10px 0;
font-size: 16px;
flex-grow: 2;
padding: 5px;
}

button {
display: inline-block;
font-size: 16px;
height: 32px;
}

</style>
</head>
<body>
<div id="container">
<div id="form-wrapper">
<form>
<input type="text" id="hash" placeholder="Hash" value="QmZ8dHcccdqNBNgEHKnSMCVjAAhLc293tmhDZZcptfF5eD" disabled />
<button id="gobutton" disabled>Go!</button>
</form>
<video id="video" controls></video>
</div>
<pre id="output" style="display: inline-block"></pre>
</div>
</body>
</html>
127 changes: 127 additions & 0 deletions examples/browser-readablestream/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
'use strict'

/* eslint-env browser */

const Ipfs = require('../../')
const videoStream = require('videostream')
const ipfs = new Ipfs({ repo: 'ipfs-' + Math.random() })

const log = (line) => {
document.getElementById('output').appendChild(document.createTextNode(`${line}\r\n`))
}

log('IPFS: Initialising')

const timeouts = []

ipfs.on('ready', () => {
// Set up event listeners on the <video> element from index.html
const videoElement = createVideoElement()
const hashInput = document.getElementById('hash')
const goButton = document.getElementById('gobutton')
let stream

goButton.onclick = function (event) {
event.preventDefault()

log(`IPFS: Playing ${hashInput.value.trim()}`)

// Set up the video stream an attach it to our <video> element
videoStream({
createReadStream: function (opts) {
const start = opts.start

// The videostream library does not always pass an end byte but when
// it does, it wants bytes between start & end inclusive.
// catReadableStream returns the bytes exclusive so increment the end
// byte if it's been requested
const end = opts.end ? start + opts.end + 1 : undefined

log(`Stream: Asked for data starting at byte ${start} and ending at byte ${end}`)

// If we've streamed before, clean up the existing stream
if (stream && stream.destroy) {
stream.destroy()
}

// This stream will contain the requested bytes
stream = ipfs.files.catReadableStream(hashInput.value.trim(), start, end)

// Log error messages
stream.on('error', log)

if (start === 0) {
// Show the user some messages while we wait for the data stream to start
statusMessages(stream)
}

return stream
}
}, videoElement)
}

log('IPFS: Ready')
log('IPFS: Press the "Go!" button to start playing a video')

hashInput.disabled = false
goButton.disabled = false
})

const createVideoElement = () => {
const videoElement = document.getElementById('video')
videoElement.addEventListener('loadedmetadata', () => {
videoElement.play()
.then(() => log('Video: Playing'))
.catch(log)
})

const events = [
'playing',
'waiting',
'seeking',
'seeked',
'ended',
'loadedmetadata',
'loadeddata',
'canplay',
'canplaythrough',
'durationchange',
'play',
'pause',
'suspend',
'emptied',
'stalled',
'error',
'abort'
]
events.forEach(event => {
videoElement.addEventListener(event, () => {
log(`Video: ${event}`)
})
})

videoElement.addEventListener('error', () => {
if (videoElement.error) {
log('Error:', videoElement.error.message)
}
})

return videoElement
}

const statusMessages = (stream) => {
let time = 0
const timeouts = [
'Stream: Still loading data from IPFS...',
'Stream: This can take a while depending on content availability',
'Stream: Hopefully not long now',
'Stream: *Whistles absentmindedly*',
'Stream: *Taps foot*',
'Stream: *Looks at watch*'
].map(message => setTimeout(() => log(message), time += 5000))

stream.once('data', () => {
log('Stream: Here we go')
timeouts.forEach(clearTimeout)
})
}
22 changes: 22 additions & 0 deletions examples/browser-readablestream/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "browser-videostream",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "npm run build && http-server dist -a 127.0.0.1 -p 8888"
},
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^2.30.1",
"http-server": "^0.11.1",
"uglifyjs-webpack-plugin": "^1.2.0",
"webpack": "^3.11.0"
},
"dependencies": {
"videostream": "^2.4.2"
}
}
29 changes: 29 additions & 0 deletions examples/browser-readablestream/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict'

const path = require('path')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
devtool: 'source-map',
entry: [
'./index.js'
],
plugins: [
new UglifyJsPlugin({
sourceMap: true,
uglifyOptions: {
mangle: false,
compress: false
}
}),
new HtmlWebpackPlugin({
title: 'IPFS Videostream example',
template: 'index.html'
})
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"ipfs-unixfs": "~0.1.14",
"ipfs-unixfs-engine": "~0.24.4",
"ipld": "^0.15.0",
"ipld-dag-pb": "^0.13.1",
"is-ipfs": "^0.3.2",
"is-stream": "^1.1.0",
"joi": "^13.1.2",
Expand All @@ -136,6 +137,7 @@
"libp2p-websockets": "~0.10.5",
"lodash.flatmap": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"lodash.sortby": "^4.7.0",
"lodash.values": "^4.3.0",
"mafmt": "^4.0.0",
Expand Down
29 changes: 23 additions & 6 deletions src/core/components/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ module.exports = function files (self) {
)
}

function _catPullStream (ipfsPath) {
function _catPullStream (ipfsPath, begin, end) {
if (typeof ipfsPath === 'function') {
throw new Error('You must supply an ipfsPath')
}
Expand All @@ -139,7 +139,10 @@ module.exports = function files (self) {
const d = deferred.source()

pull(
exporter(ipfsPath, self._ipld),
exporter(ipfsPath, self._ipld, {
begin,
end
}),
pull.collect((err, files) => {
if (err) { return d.abort(err) }
if (files && files.length > 1) {
Expand Down Expand Up @@ -230,19 +233,33 @@ module.exports = function files (self) {

addPullStream: _addPullStream,

cat: promisify((ipfsPath, callback) => {
cat: promisify((ipfsPath, begin, end, callback) => {
if (typeof begin === 'function') {
callback = begin
begin = undefined
}

if (typeof end === 'function') {
callback = end
end = undefined
}

if (typeof callback !== 'function') {
throw new Error('Please supply a callback to ipfs.files.cat')
}

pull(
_catPullStream(ipfsPath),
_catPullStream(ipfsPath, begin, end),
pull.collect((err, buffers) => {
if (err) { return callback(err) }
callback(null, Buffer.concat(buffers))
})
)
}),

catReadableStream: (ipfsPath) => toStream.source(_catPullStream(ipfsPath)),
catReadableStream: (ipfsPath, begin, end) => toStream.source(_catPullStream(ipfsPath, begin, end)),

catPullStream: _catPullStream,
catPullStream: (ipfsPath, begin, end) => _catPullStream(ipfsPath, begin, end),

get: promisify((ipfsPath, callback) => {
pull(
Expand Down

0 comments on commit b76eb50

Please sign in to comment.