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

Commit b76eb50

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 8e3ea44 commit b76eb50

File tree

8 files changed

+282
-6
lines changed

8 files changed

+282
-6
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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
</style>
49+
</head>
50+
<body>
51+
<div id="container">
52+
<div id="form-wrapper">
53+
<form>
54+
<input type="text" id="hash" placeholder="Hash" value="QmZ8dHcccdqNBNgEHKnSMCVjAAhLc293tmhDZZcptfF5eD" disabled />
55+
<button id="gobutton" disabled>Go!</button>
56+
</form>
57+
<video id="video" controls></video>
58+
</div>
59+
<pre id="output" style="display: inline-block"></pre>
60+
</div>
61+
</body>
62+
</html>
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
9+
const log = (line) => {
10+
document.getElementById('output').appendChild(document.createTextNode(`${line}\r\n`))
11+
}
12+
13+
log('IPFS: Initialising')
14+
15+
const timeouts = []
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 (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(), start, end)
49+
50+
// Log error messages
51+
stream.on('error', log)
52+
53+
if (start === 0) {
54+
// Show the user some messages while we wait for the data stream to start
55+
statusMessages(stream)
56+
}
57+
58+
return stream
59+
}
60+
}, videoElement)
61+
}
62+
63+
log('IPFS: Ready')
64+
log('IPFS: Press the "Go!" button to start playing a video')
65+
66+
hashInput.disabled = false
67+
goButton.disabled = false
68+
})
69+
70+
const createVideoElement = () => {
71+
const videoElement = document.getElementById('video')
72+
videoElement.addEventListener('loadedmetadata', () => {
73+
videoElement.play()
74+
.then(() => log('Video: Playing'))
75+
.catch(log)
76+
})
77+
78+
const events = [
79+
'playing',
80+
'waiting',
81+
'seeking',
82+
'seeked',
83+
'ended',
84+
'loadedmetadata',
85+
'loadeddata',
86+
'canplay',
87+
'canplaythrough',
88+
'durationchange',
89+
'play',
90+
'pause',
91+
'suspend',
92+
'emptied',
93+
'stalled',
94+
'error',
95+
'abort'
96+
]
97+
events.forEach(event => {
98+
videoElement.addEventListener(event, () => {
99+
log(`Video: ${event}`)
100+
})
101+
})
102+
103+
videoElement.addEventListener('error', () => {
104+
if (videoElement.error) {
105+
log('Error:', videoElement.error.message)
106+
}
107+
})
108+
109+
return videoElement
110+
}
111+
112+
const statusMessages = (stream) => {
113+
let time = 0
114+
const timeouts = [
115+
'Stream: Still loading data from IPFS...',
116+
'Stream: This can take a while depending on content availability',
117+
'Stream: Hopefully not long now',
118+
'Stream: *Whistles absentmindedly*',
119+
'Stream: *Taps foot*',
120+
'Stream: *Looks at watch*'
121+
].map(message => setTimeout(() => log(message), time += 5000))
122+
123+
stream.once('data', () => {
124+
log('Stream: Here we go')
125+
timeouts.forEach(clearTimeout)
126+
})
127+
}
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: 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"ipfs-unixfs": "~0.1.14",
117117
"ipfs-unixfs-engine": "~0.24.4",
118118
"ipld": "^0.15.0",
119+
"ipld-dag-pb": "^0.13.1",
119120
"is-ipfs": "^0.3.2",
120121
"is-stream": "^1.1.0",
121122
"joi": "^13.1.2",
@@ -136,6 +137,7 @@
136137
"libp2p-websockets": "~0.10.5",
137138
"lodash.flatmap": "^4.5.0",
138139
"lodash.get": "^4.4.2",
140+
"lodash.set": "^4.3.2",
139141
"lodash.sortby": "^4.7.0",
140142
"lodash.values": "^4.3.0",
141143
"mafmt": "^4.0.0",

src/core/components/files.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ module.exports = function files (self) {
126126
)
127127
}
128128

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

141141
pull(
142-
exporter(ipfsPath, self._ipld),
142+
exporter(ipfsPath, self._ipld, {
143+
begin,
144+
end
145+
}),
143146
pull.collect((err, files) => {
144147
if (err) { return d.abort(err) }
145148
if (files && files.length > 1) {
@@ -230,19 +233,33 @@ module.exports = function files (self) {
230233

231234
addPullStream: _addPullStream,
232235

233-
cat: promisify((ipfsPath, callback) => {
236+
cat: promisify((ipfsPath, begin, end, callback) => {
237+
if (typeof begin === 'function') {
238+
callback = begin
239+
begin = undefined
240+
}
241+
242+
if (typeof end === 'function') {
243+
callback = end
244+
end = undefined
245+
}
246+
247+
if (typeof callback !== 'function') {
248+
throw new Error('Please supply a callback to ipfs.files.cat')
249+
}
250+
234251
pull(
235-
_catPullStream(ipfsPath),
252+
_catPullStream(ipfsPath, begin, end),
236253
pull.collect((err, buffers) => {
237254
if (err) { return callback(err) }
238255
callback(null, Buffer.concat(buffers))
239256
})
240257
)
241258
}),
242259

243-
catReadableStream: (ipfsPath) => toStream.source(_catPullStream(ipfsPath)),
260+
catReadableStream: (ipfsPath, begin, end) => toStream.source(_catPullStream(ipfsPath, begin, end)),
244261

245-
catPullStream: _catPullStream,
262+
catPullStream: (ipfsPath, begin, end) => _catPullStream(ipfsPath, begin, end),
246263

247264
get: promisify((ipfsPath, callback) => {
248265
pull(

0 commit comments

Comments
 (0)