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 21, 2018
1 parent c1e8db1 commit 580c1f3
Show file tree
Hide file tree
Showing 8 changed files with 245 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" 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>
90 changes: 90 additions & 0 deletions examples/browser-readablestream/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use strict'

/* eslint-env browser */

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

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

log('Initialising IPFS')

let stream

const cleanUp = () => {
if (stream && stream.destroy) {
stream.destroy()
}
}

ipfs.on('ready', () => {
const videoElement = createVideoElement()

log('Adding video file')

addVideoFile('/video.mp4')
.then(hash => {
log(`Added file with hash ${hash}`)

const hashInput = document.getElementById('hash')
const goButton = document.getElementById('gobutton')

hashInput.value = hash

goButton.onclick = function () {
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(`Asked for data starting at byte ${start} and ending at byte ${end}`)

cleanUp()

// We will write the requested bytes into this stream
stream = ipfs.files.catReadableStream(hashInput.value.trim(), start, end)

return stream
}
}, videoElement)

return false
}

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

const addVideoFile = (path) => {
return fetch(path)
.then(response => response.arrayBuffer())
.then(buffer => ipfs.files.add(Buffer.from(buffer)))
.then(result => result.pop().hash)
}

const createVideoElement = () => {
const videoElement = document.getElementById('video')
videoElement.addEventListener('loadedmetadata', () => {
log('Video metadata loaded')

videoElement.play()
})
videoElement.addEventListener('loadeddata', () => {
log('First video frame loaded')
})
videoElement.addEventListener('loadstart', () => {
log('Started loading video')
})

return videoElement
}
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 && curl https://www.html5rocks.com/en/tutorials/video/basics/devstories.mp4 -o dist/video.mp4 && 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, ipldResolver, 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, self._ipldResolver, begin, end)),

catPullStream: _catPullStream,
catPullStream: (ipfsPath, begin, end) => _catPullStream(ipfsPath, self._ipldResolver, begin, end),

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

0 comments on commit 580c1f3

Please sign in to comment.