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

Commit b9b2564

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 93d2bf5 commit b9b2564

File tree

12 files changed

+444
-18
lines changed

12 files changed

+444
-18
lines changed

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Let us know if you find any issue or if you want to contribute and add a new tut
2121
- [js-ipfs in the browser with a `<script>` tag](./browser-script-tag)
2222
- [js-ipfs in electron](./run-in-electron)
2323
- [Using streams to add a directory of files to ipfs](./browser-add-readable-stream)
24+
- [Streaming video from ipfs to the browser using `ReadableStream`s](./browser-readablestream)
2425

2526
## Understanding the IPFS Stack
2627

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: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
const timeouts = []
18+
19+
ipfs.on('ready', () => {
20+
// Set up event listeners on the <video> element from index.html
21+
const videoElement = createVideoElement()
22+
const hashInput = document.getElementById('hash')
23+
const goButton = document.getElementById('gobutton')
24+
let stream
25+
26+
goButton.onclick = function (event) {
27+
event.preventDefault()
28+
29+
log(`IPFS: Playing ${hashInput.value.trim()}`)
30+
31+
// Set up the video stream an attach it to our <video> element
32+
videoStream({
33+
createReadStream: function createReadStream (opts) {
34+
const start = opts.start
35+
36+
// The videostream library does not always pass an end byte but when
37+
// it does, it wants bytes between start & end inclusive.
38+
// catReadableStream returns the bytes exclusive so increment the end
39+
// byte if it's been requested
40+
const end = opts.end ? start + opts.end + 1 : undefined
41+
42+
log(`Stream: Asked for data starting at byte ${start} and ending at byte ${end}`)
43+
44+
// If we've streamed before, clean up the existing stream
45+
if (stream && stream.destroy) {
46+
stream.destroy()
47+
}
48+
49+
// This stream will contain the requested bytes
50+
stream = ipfs.files.catReadableStream(hashInput.value.trim(), {
51+
offset: start,
52+
length: end && end - start
53+
})
54+
55+
// Log error messages
56+
stream.on('error', (error) => log(error))
57+
58+
if (start === 0) {
59+
// Show the user some messages while we wait for the data stream to start
60+
statusMessages(stream, log)
61+
}
62+
63+
return stream
64+
}
65+
}, videoElement)
66+
}
67+
68+
// Allow adding files to IPFS via drag and drop
69+
dragDrop(ipfs, log)
70+
71+
log('IPFS: Ready')
72+
log('IPFS: Drop an .mp4 file into this window to add a file')
73+
log('IPFS: Then press the "Go!" button to start playing a video')
74+
75+
hashInput.disabled = false
76+
goButton.disabled = false
77+
})
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: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
const log = (line) => {
2+
const output = document.getElementById('output')
3+
let message
4+
5+
if (line.message) {
6+
message = `Error: ${line.message.toString()}`
7+
} else {
8+
message = line
9+
}
10+
11+
if (message) {
12+
const node = document.createTextNode(`${message}\r\n`)
13+
output.appendChild(node)
14+
15+
output.scrollTop = output.offsetHeight
16+
17+
return node
18+
}
19+
}
20+
21+
const dragDrop = (ipfs) => {
22+
const container = document.querySelector('#container')
23+
24+
container.ondragover = (event) => {
25+
event.preventDefault()
26+
}
27+
28+
container.ondrop = (event) => {
29+
event.preventDefault()
30+
31+
for (let i = 0; i < event.dataTransfer.items.length; i++) {
32+
const item = event.dataTransfer.items[i]
33+
34+
if (item.kind !== 'file') {
35+
continue
36+
}
37+
38+
const file = item.getAsFile()
39+
40+
const progress = log(`IPFS: Adding ${file.name} 0%`)
41+
42+
const reader = new window.FileReader()
43+
reader.onload = (event) => {
44+
ipfs.files.add({
45+
path: file.name,
46+
content: ipfs.types.Buffer.from(event.target.result)
47+
}, {
48+
progress: (addedBytes) => {
49+
progress.textContent = `IPFS: Adding ${file.name} ${parseInt((addedBytes / file.size) * 100)}%\r\n`
50+
}
51+
}, (error, added) => {
52+
if (error) {
53+
return log(error)
54+
}
55+
56+
const hash = added[0].hash
57+
58+
log(`IPFS: Added ${hash}`)
59+
60+
document.querySelector('#hash').value = hash
61+
})
62+
}
63+
reader.readAsArrayBuffer(file)
64+
}
65+
66+
if (event.dataTransfer.items && event.dataTransfer.items.clear) {
67+
event.dataTransfer.items.clear();
68+
}
69+
70+
if (event.dataTransfer.clearData) {
71+
event.dataTransfer.clearData();
72+
}
73+
}
74+
}
75+
76+
module.exports.statusMessages = (stream) => {
77+
let time = 0
78+
const timeouts = [
79+
'Stream: Still loading data from IPFS...',
80+
'Stream: This can take a while depending on content availability',
81+
'Stream: Hopefully not long now',
82+
'Stream: *Whistles absentmindedly*',
83+
'Stream: *Taps foot*',
84+
'Stream: *Looks at watch*',
85+
'Stream: *Stares at floor*',
86+
'Stream: *Checks phone*',
87+
'Stream: *Stares at ceiling*',
88+
'Stream: Got anything nice planned for the weekend?'
89+
].map(message => setTimeout(() => log(message), time += 5000))
90+
91+
stream.once('data', () => {
92+
log('Stream: Started receiving data')
93+
timeouts.forEach(clearTimeout)
94+
})
95+
stream.once('error', () => {
96+
timeouts.forEach(clearTimeout)
97+
})
98+
}
99+
100+
const createVideoElement = () => {
101+
const videoElement = document.getElementById('video')
102+
videoElement.addEventListener('loadedmetadata', () => {
103+
videoElement.play()
104+
.catch(log)
105+
})
106+
107+
const events = [
108+
'playing',
109+
'waiting',
110+
'seeking',
111+
'seeked',
112+
'ended',
113+
'loadedmetadata',
114+
'loadeddata',
115+
'canplay',
116+
'canplaythrough',
117+
'durationchange',
118+
'play',
119+
'pause',
120+
'suspend',
121+
'emptied',
122+
'stalled',
123+
'error',
124+
'abort'
125+
]
126+
events.forEach(event => {
127+
videoElement.addEventListener(event, () => {
128+
log(`Video: ${event}`)
129+
})
130+
})
131+
132+
videoElement.addEventListener('error', () => {
133+
log(videoElement.error)
134+
})
135+
136+
return videoElement
137+
}
138+
139+
module.exports.log = log
140+
module.exports.dragDrop = dragDrop
141+
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
@@ -106,7 +106,7 @@
106106
"hapi-set-header": "^1.0.2",
107107
"hoek": "^5.0.3",
108108
"human-to-milliseconds": "^1.0.0",
109-
"ipfs-api": "^19.0.0",
109+
"ipfs-api": "^20.0.0",
110110
"ipfs-bitswap": "~0.19.0",
111111
"ipfs-block": "~0.6.1",
112112
"ipfs-block-service": "~0.13.0",
@@ -115,6 +115,7 @@
115115
"ipfs-unixfs": "~0.1.14",
116116
"ipfs-unixfs-engine": "~0.27.0",
117117
"ipld": "^0.15.0",
118+
"ipld-dag-pb": "^0.13.1",
118119
"is-ipfs": "^0.3.2",
119120
"is-stream": "^1.1.0",
120121
"joi": "^13.1.2",
@@ -135,6 +136,7 @@
135136
"libp2p-websockets": "~0.10.5",
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": "^4.0.0",

0 commit comments

Comments
 (0)