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

Commit e33e668

Browse files
committed
docs: Add browser example for ReadableStreams
feat: Allows for byte offsets when using ipfs.files.cat and friends to request slices of files
1 parent 3019fc0 commit e33e668

File tree

12 files changed

+446
-18
lines changed

12 files changed

+446
-18
lines changed

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Let us know if you find any issue or if you want to contribute and add a new tut
2222
- [js-ipfs in electron](./run-in-electron)
2323
- [Using streams to add a directory of files to ipfs](./browser-add-readable-stream)
2424
- [Customizing the ipfs repository](./custom-ipfs-repo)
25+
- - [Streaming video from ipfs to the browser using `ReadableStream`s](./browser-readablestream)
2526

2627
## Understanding the IPFS Stack
2728

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Streaming video from IPFS using ReadableStreams
2+
3+
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.
4+
5+
Take a look at [`index.js`](./index.js) to see a working example.
6+
7+
## Running the demo
8+
9+
In this directory:
10+
11+
```
12+
$ npm install
13+
$ npm start
14+
```
15+
16+
Then open [http://localhost:8888](http://localhost:8888) in your browser.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
5+
<title><%= htmlWebpackPlugin.options.title %></title>
6+
<style type="text/css">
7+
8+
body {
9+
margin: 0;
10+
padding: 0;
11+
}
12+
13+
#container {
14+
display: flex;
15+
height: 100vh;
16+
}
17+
18+
pre {
19+
flex-grow: 2;
20+
padding: 10px;
21+
height: calc(100vh - 45px);
22+
overflow: auto;
23+
}
24+
25+
#form-wrapper {
26+
padding: 20px;
27+
}
28+
29+
form {
30+
padding-bottom: 10px;
31+
display: flex;
32+
}
33+
34+
#hash {
35+
display: inline-block;
36+
margin: 0 10px 10px 0;
37+
font-size: 16px;
38+
flex-grow: 2;
39+
padding: 5px;
40+
}
41+
42+
button {
43+
display: inline-block;
44+
font-size: 16px;
45+
height: 32px;
46+
}
47+
48+
video {
49+
max-width: 50vw;
50+
}
51+
52+
</style>
53+
</head>
54+
<body>
55+
<div id="container" ondrop="dropHandler(event)" ondragover="dragOverHandler(event)">
56+
<div id="form-wrapper">
57+
<form>
58+
<input type="text" id="hash" placeholder="Hash" disabled />
59+
<button id="gobutton" disabled>Go!</button>
60+
</form>
61+
<video id="video" controls></video>
62+
</div>
63+
<pre id="output" style="display: inline-block"></pre>
64+
</div>
65+
</body>
66+
</html>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict'
2+
3+
/* eslint-env browser */
4+
5+
const Ipfs = require('../../')
6+
const videoStream = require('videostream')
7+
const ipfs = new Ipfs({ repo: 'ipfs-' + Math.random() })
8+
const {
9+
dragDrop,
10+
statusMessages,
11+
createVideoElement,
12+
log
13+
} = require('./utils')
14+
15+
log('IPFS: Initialising')
16+
17+
ipfs.on('ready', () => {
18+
// Set up event listeners on the <video> element from index.html
19+
const videoElement = createVideoElement()
20+
const hashInput = document.getElementById('hash')
21+
const goButton = document.getElementById('gobutton')
22+
let stream
23+
24+
goButton.onclick = function (event) {
25+
event.preventDefault()
26+
27+
log(`IPFS: Playing ${hashInput.value.trim()}`)
28+
29+
// Set up the video stream an attach it to our <video> element
30+
videoStream({
31+
createReadStream: function createReadStream (opts) {
32+
const start = opts.start
33+
34+
// The videostream library does not always pass an end byte but when
35+
// it does, it wants bytes between start & end inclusive.
36+
// catReadableStream returns the bytes exclusive so increment the end
37+
// byte if it's been requested
38+
const end = opts.end ? start + opts.end + 1 : undefined
39+
40+
log(`Stream: Asked for data starting at byte ${start} and ending at byte ${end}`)
41+
42+
// If we've streamed before, clean up the existing stream
43+
if (stream && stream.destroy) {
44+
stream.destroy()
45+
}
46+
47+
// This stream will contain the requested bytes
48+
stream = ipfs.files.catReadableStream(hashInput.value.trim(), {
49+
offset: start,
50+
length: end && end - start
51+
})
52+
53+
// Log error messages
54+
stream.on('error', (error) => log(error))
55+
56+
if (start === 0) {
57+
// Show the user some messages while we wait for the data stream to start
58+
statusMessages(stream, log)
59+
}
60+
61+
return stream
62+
}
63+
}, videoElement)
64+
}
65+
66+
// Allow adding files to IPFS via drag and drop
67+
dragDrop(ipfs, log)
68+
69+
log('IPFS: Ready')
70+
log('IPFS: Drop an .mp4 file into this window to add a file')
71+
log('IPFS: Then press the "Go!" button to start playing a video')
72+
73+
hashInput.disabled = false
74+
goButton.disabled = false
75+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "browser-videostream",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"build": "webpack",
9+
"start": "npm run build && http-server dist -a 127.0.0.1 -p 8888"
10+
},
11+
"author": "",
12+
"license": "ISC",
13+
"devDependencies": {
14+
"html-webpack-plugin": "^2.30.1",
15+
"http-server": "^0.11.1",
16+
"uglifyjs-webpack-plugin": "^1.2.0",
17+
"webpack": "^3.11.0"
18+
},
19+
"dependencies": {
20+
"videostream": "^2.4.2"
21+
}
22+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
'use strict'
2+
3+
const log = (line) => {
4+
const output = document.getElementById('output')
5+
let message
6+
7+
if (line.message) {
8+
message = `Error: ${line.message.toString()}`
9+
} else {
10+
message = line
11+
}
12+
13+
if (message) {
14+
const node = document.createTextNode(`${message}\r\n`)
15+
output.appendChild(node)
16+
17+
output.scrollTop = output.offsetHeight
18+
19+
return node
20+
}
21+
}
22+
23+
const dragDrop = (ipfs) => {
24+
const container = document.querySelector('#container')
25+
26+
container.ondragover = (event) => {
27+
event.preventDefault()
28+
}
29+
30+
container.ondrop = (event) => {
31+
event.preventDefault()
32+
33+
Array.prototype.slice.call(event.dataTransfer.items)
34+
.filter(item => item.kind === 'file')
35+
.map(item => item.getAsFile())
36+
.forEach(file => {
37+
const progress = log(`IPFS: Adding ${file.name} 0%`)
38+
39+
const reader = new window.FileReader()
40+
reader.onload = (event) => {
41+
ipfs.files.add({
42+
path: file.name,
43+
content: ipfs.types.Buffer.from(event.target.result)
44+
}, {
45+
progress: (addedBytes) => {
46+
progress.textContent = `IPFS: Adding ${file.name} ${parseInt((addedBytes / file.size) * 100)}%\r\n`
47+
}
48+
}, (error, added) => {
49+
if (error) {
50+
return log(error)
51+
}
52+
53+
const hash = added[0].hash
54+
55+
log(`IPFS: Added ${hash}`)
56+
57+
document.querySelector('#hash').value = hash
58+
})
59+
}
60+
61+
reader.readAsArrayBuffer(file)
62+
})
63+
64+
if (event.dataTransfer.items && event.dataTransfer.items.clear) {
65+
event.dataTransfer.items.clear()
66+
}
67+
68+
if (event.dataTransfer.clearData) {
69+
event.dataTransfer.clearData()
70+
}
71+
}
72+
}
73+
74+
module.exports.statusMessages = (stream) => {
75+
let time = 0
76+
const timeouts = [
77+
'Stream: Still loading data from IPFS...',
78+
'Stream: This can take a while depending on content availability',
79+
'Stream: Hopefully not long now',
80+
'Stream: *Whistles absentmindedly*',
81+
'Stream: *Taps foot*',
82+
'Stream: *Looks at watch*',
83+
'Stream: *Stares at floor*',
84+
'Stream: *Checks phone*',
85+
'Stream: *Stares at ceiling*',
86+
'Stream: Got anything nice planned for the weekend?'
87+
].map(message => {
88+
time += 5000
89+
90+
return setTimeout(() => {
91+
log(message)
92+
}, time)
93+
})
94+
95+
stream.once('data', () => {
96+
log('Stream: Started receiving data')
97+
timeouts.forEach(clearTimeout)
98+
})
99+
stream.once('error', () => {
100+
timeouts.forEach(clearTimeout)
101+
})
102+
}
103+
104+
const createVideoElement = () => {
105+
const videoElement = document.getElementById('video')
106+
videoElement.addEventListener('loadedmetadata', () => {
107+
videoElement.play()
108+
.catch(log)
109+
})
110+
111+
const events = [
112+
'playing',
113+
'waiting',
114+
'seeking',
115+
'seeked',
116+
'ended',
117+
'loadedmetadata',
118+
'loadeddata',
119+
'canplay',
120+
'canplaythrough',
121+
'durationchange',
122+
'play',
123+
'pause',
124+
'suspend',
125+
'emptied',
126+
'stalled',
127+
'error',
128+
'abort'
129+
]
130+
events.forEach(event => {
131+
videoElement.addEventListener(event, () => {
132+
log(`Video: ${event}`)
133+
})
134+
})
135+
136+
videoElement.addEventListener('error', () => {
137+
log(videoElement.error)
138+
})
139+
140+
return videoElement
141+
}
142+
143+
module.exports.log = log
144+
module.exports.dragDrop = dragDrop
145+
module.exports.createVideoElement = createVideoElement
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict'
2+
3+
const path = require('path')
4+
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
5+
const HtmlWebpackPlugin = require('html-webpack-plugin')
6+
7+
module.exports = {
8+
devtool: 'source-map',
9+
entry: [
10+
'./index.js'
11+
],
12+
plugins: [
13+
new UglifyJsPlugin({
14+
sourceMap: true,
15+
uglifyOptions: {
16+
mangle: false,
17+
compress: false
18+
}
19+
}),
20+
new HtmlWebpackPlugin({
21+
title: 'IPFS Videostream example',
22+
template: 'index.html'
23+
})
24+
],
25+
output: {
26+
path: path.join(__dirname, 'dist'),
27+
filename: 'bundle.js'
28+
}
29+
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"expose-loader": "^0.7.5",
7474
"form-data": "^2.3.2",
7575
"hat": "0.0.3",
76-
"interface-ipfs-core": "^0.61.0",
76+
"interface-ipfs-core": "~0.64.2",
7777
"ipfsd-ctl": "^0.32.1",
7878
"lodash": "^4.17.10",
7979
"mocha": "^5.1.1",
@@ -116,6 +116,7 @@
116116
"ipfs-unixfs-engine": "~0.29.0",
117117
"ipld": "^0.17.0",
118118
"is-ipfs": "^0.3.2",
119+
"ipld-dag-pb": "~0.14.3",
119120
"is-stream": "^1.1.0",
120121
"joi": "^13.2.0",
121122
"joi-browser": "^13.0.1",
@@ -135,6 +136,7 @@
135136
"libp2p-websockets": "~0.12.0",
136137
"lodash.flatmap": "^4.5.0",
137138
"lodash.get": "^4.4.2",
139+
"lodash.set": "^4.3.2",
138140
"lodash.sortby": "^4.7.0",
139141
"lodash.values": "^4.3.0",
140142
"mafmt": "^6.0.0",

0 commit comments

Comments
 (0)