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

Commit 1601f6f

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 c1e8db1 commit 1601f6f

File tree

8 files changed

+246
-6
lines changed

8 files changed

+246
-6
lines changed

examples/README.md

+1
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

+16
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.
+62
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" 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>
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use strict'
2+
3+
/* eslint-env browser */
4+
5+
const Ipfs = require('../../')
6+
const videoStream = require('videostream')
7+
const repoPath = 'ipfs-' + Math.random()
8+
const ipfs = new Ipfs({ repo: repoPath })
9+
10+
const log = (line) => {
11+
document.getElementById('output').appendChild(document.createTextNode(`${line}\r\n`))
12+
}
13+
14+
log('Initialising IPFS')
15+
16+
let stream
17+
18+
const cleanUp = () => {
19+
if (stream && stream.destroy) {
20+
stream.destroy()
21+
}
22+
}
23+
24+
ipfs.on('ready', () => {
25+
const videoElement = createVideoElement()
26+
27+
log('Adding video file')
28+
29+
addVideoFile('/video.mp4')
30+
.then(hash => {
31+
log(`Added file with hash ${hash}`)
32+
33+
const hashInput = document.getElementById('hash')
34+
const goButton = document.getElementById('gobutton')
35+
36+
hashInput.value = hash
37+
38+
goButton.onclick = function () {
39+
videoStream({
40+
createReadStream: function (opts) {
41+
const start = opts.start
42+
43+
// The videostream library does not always pass an end byte but when
44+
// it does, it wants bytes between start & end inclusive.
45+
// catReadableStream returns the bytes exclusive so increment the end
46+
// byte if it's been requested
47+
const end = opts.end ? start + opts.end + 1 : undefined
48+
49+
log(`Asked for data starting at byte ${start} and ending at byte ${end}`)
50+
51+
cleanUp()
52+
53+
// We will write the requested bytes into this stream
54+
stream = ipfs.files.catReadableStream(hashInput.value.trim(), start, end)
55+
56+
return stream
57+
}
58+
}, videoElement)
59+
60+
return false
61+
}
62+
63+
hashInput.disabled = false
64+
goButton.disabled = false
65+
})
66+
})
67+
68+
const addVideoFile = (path) => {
69+
return fetch(path)
70+
.then(response => response.arrayBuffer())
71+
.then(buffer => ipfs.files.add(Buffer.from(buffer)))
72+
.then(result => result.pop().hash)
73+
}
74+
75+
const createVideoElement = () => {
76+
const videoElement = document.getElementById('video')
77+
videoElement.addEventListener('loadedmetadata', () => {
78+
log('Video metadata loaded')
79+
80+
videoElement.play()
81+
})
82+
videoElement.addEventListener('loadeddata', () => {
83+
log('First video frame loaded')
84+
})
85+
videoElement.addEventListener('loadstart', () => {
86+
log('Started loading video')
87+
})
88+
89+
return videoElement
90+
}
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 && 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"
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+
}
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

+2
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

+24-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const unixfsEngine = require('ipfs-unixfs-engine')
44
const importer = unixfsEngine.importer
55
const exporter = unixfsEngine.exporter
6+
const reader = unixfsEngine.reader
67
const promisify = require('promisify-es6')
78
const pull = require('pull-stream')
89
const sort = require('pull-sort')
@@ -126,7 +127,7 @@ module.exports = function files (self) {
126127
)
127128
}
128129

129-
function _catPullStream (ipfsPath) {
130+
function _catPullStream (ipfsPath, ipldResolver, begin, end) {
130131
if (typeof ipfsPath === 'function') {
131132
throw new Error('You must supply an ipfsPath')
132133
}
@@ -139,7 +140,10 @@ module.exports = function files (self) {
139140
const d = deferred.source()
140141

141142
pull(
142-
exporter(ipfsPath, self._ipld),
143+
exporter(ipfsPath, self._ipld, {
144+
begin,
145+
end
146+
}),
143147
pull.collect((err, files) => {
144148
if (err) { return d.abort(err) }
145149
if (files && files.length > 1) {
@@ -230,19 +234,33 @@ module.exports = function files (self) {
230234

231235
addPullStream: _addPullStream,
232236

233-
cat: promisify((ipfsPath, callback) => {
237+
cat: promisify((ipfsPath, begin, end, callback) => {
238+
if (typeof begin === 'function') {
239+
callback = begin
240+
begin = undefined
241+
}
242+
243+
if (typeof end === 'function') {
244+
callback = end
245+
end = undefined
246+
}
247+
248+
if (typeof callback !== 'function') {
249+
throw new Error('Please supply a callback to ipfs.files.cat')
250+
}
251+
234252
pull(
235-
_catPullStream(ipfsPath),
253+
_catPullStream(ipfsPath, begin, end),
236254
pull.collect((err, buffers) => {
237255
if (err) { return callback(err) }
238256
callback(null, Buffer.concat(buffers))
239257
})
240258
)
241259
}),
242260

243-
catReadableStream: (ipfsPath) => toStream.source(_catPullStream(ipfsPath)),
261+
catReadableStream: (ipfsPath, begin, end) => toStream.source(_catPullStream(ipfsPath, self._ipldResolver, begin, end)),
244262

245-
catPullStream: _catPullStream,
263+
catPullStream: (ipfsPath, begin, end) => _catPullStream(ipfsPath, self._ipldResolver, begin, end),
246264

247265
get: promisify((ipfsPath, callback) => {
248266
pull(

0 commit comments

Comments
 (0)