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

Commit 8f8c539

Browse files
committed
docs: Add browser example for ReadableStreams
1 parent e4d2a15 commit 8f8c539

File tree

6 files changed

+368
-0
lines changed

6 files changed

+368
-0
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 two approaches to do this.
6+
7+
### ipfs.files.catReadablestream
8+
9+
The first is to use the high level [`ipfs.files.catReadablestream`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#catreadablestream) function to get file contents. It's a naive approach which throws away unwanted bytes until it finds the requested range. E.g. say bytes 100-200 are requested by `videostream`, this approach will read and disgard bytes 0-99 which can be expensive if you are streaming a large file.
10+
11+
### ipfs.object.data
12+
13+
The second approach uses the slightly lower level [`ipfs.object.data`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/OBJECT.md#objectdata) function to walk the list of [`DAGNode`](https://github.com/ipld/js-ipld-dag-pb#dagnode-instance-methods-and-properties)s that make up an object and read only the data that has been requested.
14+
15+
## Running the demo
16+
17+
In this directory:
18+
19+
```
20+
$ npm install
21+
$ npm start
22+
```
23+
24+
Then open [http://localhost:8888](http://localhost:8888) in your browser.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
}
32+
33+
#hash {
34+
display: block;
35+
margin-bottom: 10px;
36+
width: 100%;
37+
font-size: 16px;
38+
}
39+
40+
</style>
41+
</head>
42+
<body>
43+
<div id="container">
44+
<div id="form-wrapper">
45+
<form>
46+
<input type="text" id="hash" placeholder="Hash" disabled />
47+
<input type="radio" name="method" id="readablestream" checked disabled /> ipfs.files.catReadableStream
48+
<input type="radio" name="method" id="objectdata" disabled /> ipfs.object.data
49+
<button id="gobutton" disabled>Go!</button>
50+
</form>
51+
<video id="video" controls></video>
52+
</div>
53+
<pre id="output" style="display: inline-block"></pre>
54+
</div>
55+
</body>
56+
</html>
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
'use strict'
2+
3+
/* eslint-env browser */
4+
5+
const Ipfs = require('ipfs')
6+
const videoStream = require('videostream')
7+
const repoPath = 'ipfs-' + Math.random()
8+
const ipfs = new Ipfs({ repo: repoPath })
9+
const through = require('through')
10+
const unixFs = require('ipfs-unixfs')
11+
12+
const log = (line) => {
13+
document.getElementById('output').appendChild(document.createTextNode(`${line}\r\n`))
14+
}
15+
16+
log('Initialising IPFS')
17+
18+
let cleanUp = () => {}
19+
20+
ipfs.on('ready', () => {
21+
const videoElement = document.getElementById('video')
22+
videoElement.addEventListener('loadedmetadata', () => {
23+
log('Video metadata loaded')
24+
25+
videoElement.play()
26+
})
27+
videoElement.addEventListener('loadeddata', () => {
28+
log('First video frame loaded')
29+
})
30+
videoElement.addEventListener('loadstart', () => {
31+
log('Started loading video')
32+
})
33+
34+
log('Adding video file')
35+
36+
addVideoFile('/video.mp4')
37+
.then(hash => {
38+
log(`Added file with hash ${hash}`)
39+
log('Some other hashes you can use:')
40+
log('QmaSwSN1UkRszNu34rQ4JrbLxnX2LbyFAJnygAWmNWxt5C')
41+
log(`Choose a streaming option from the left and click 'Go!'`)
42+
43+
const hashInput = document.getElementById('hash')
44+
const goButton = document.getElementById('gobutton')
45+
const byStream = document.getElementById('readablestream')
46+
const byData = document.getElementById('objectdata')
47+
48+
hashInput.value = hash
49+
50+
goButton.onclick = function () {
51+
cleanUp()
52+
53+
if (byStream.checked) {
54+
log('Using ipfs.files.catReadableStream to play video')
55+
56+
cleanUp = playByStream(hashInput.value.trim(), videoElement)
57+
} else {
58+
log('Using ipfs.object.data to play video')
59+
60+
cleanUp = playByLinks(hashInput.value.trim(), videoElement)
61+
}
62+
63+
return false
64+
}
65+
66+
hashInput.disabled = false
67+
goButton.disabled = false
68+
byStream.disabled = false
69+
byData.disabled = false
70+
})
71+
})
72+
73+
const playByLinks = (hash, videoElement) => {
74+
let stream
75+
76+
const cleanUp = () => {
77+
if (stream && stream.destroy) {
78+
stream.destroy()
79+
}
80+
}
81+
82+
ipfs.object.links(hash)
83+
.then(links => {
84+
videoStream({
85+
createReadStream: function (opts) {
86+
// Return a readable stream that provides the bytes
87+
// between offsets "start" and "end" inclusive
88+
const start = opts.start
89+
const end = opts.end ? opts.end + 1 : undefined
90+
91+
log(`Asked for data starting at byte ${start}`)
92+
93+
cleanUp()
94+
95+
stream = offsetStream(links, start, end)
96+
97+
return stream
98+
}
99+
}, videoElement)
100+
})
101+
.catch(error => log(error.message))
102+
103+
return cleanUp
104+
}
105+
106+
const playByStream = (hash, videoElement) => {
107+
let passThroughStream
108+
let fileStream
109+
110+
const cleanUp = () => {
111+
if (passThroughStream && passThroughStream.destroy) {
112+
passThroughStream.destroy()
113+
}
114+
115+
if (fileStream && fileStream.destroy) {
116+
fileStream.destroy()
117+
}
118+
}
119+
120+
videoStream({
121+
createReadStream: function (opts) {
122+
// Return a readable stream that provides the bytes
123+
// between offsets "start" and "end" inclusive
124+
const start = opts.start
125+
let offset = 0
126+
127+
log(`Asked for data starting at byte ${start}`)
128+
129+
cleanUp()
130+
131+
// We will write the requested bytes into this stream
132+
const passThroughStream = through()
133+
134+
// Get the requested file from IPFS and seek through the stream util
135+
// we find the requested bytes
136+
const fileStream = ipfs.files.catReadableStream(hash)
137+
fileStream.on('data', (data) => {
138+
if (offset > start) {
139+
// If we've passed the requested start just pass everything through
140+
passThroughStream.write(data)
141+
} else if (offset + data.length > start) {
142+
// If the requested start is in the middle of our data, pass a slice through
143+
const begin = start - offset
144+
145+
passThroughStream.write(data.slice(begin))
146+
}
147+
148+
offset += data.length
149+
})
150+
151+
return passThroughStream
152+
}
153+
}, videoElement)
154+
155+
return cleanUp
156+
}
157+
158+
const offsetStream = (links, startByte, endByte) => {
159+
// We will write the requested bytes into this stream
160+
const stream = through()
161+
let streamPosition = 0
162+
163+
series(links.map((link, index) => {
164+
return () => {
165+
if (!stream.writable) {
166+
// The stream has been closed
167+
return
168+
}
169+
170+
// DAGNode Links report unixfs object data sizes 14 bytes larger due to the protobuf wrapper
171+
const bytesInLinkedObjectData = link.size - 14
172+
173+
if (startByte > (streamPosition + bytesInLinkedObjectData)) {
174+
// Start byte is after this block so skip it
175+
streamPosition += bytesInLinkedObjectData
176+
177+
return
178+
}
179+
180+
if (endByte && endByte < streamPosition) {
181+
// End byte was before this block so skip it
182+
streamPosition += bytesInLinkedObjectData
183+
184+
return
185+
}
186+
187+
return ipfs.object.data(link.multihash)
188+
.then(data => unixFs.unmarshal(data).data)
189+
.then(data => {
190+
if (!stream.writable) {
191+
// The stream was closed while we were getting data
192+
return
193+
}
194+
195+
const length = data.length
196+
197+
if (startByte > streamPosition && startByte < (streamPosition + length)) {
198+
// If the startByte is in the current block, skip to the startByte
199+
data = data.slice(startByte - streamPosition)
200+
}
201+
202+
stream.write(data)
203+
204+
streamPosition += length
205+
})
206+
}
207+
}))
208+
.catch(error => log(error.message))
209+
210+
return stream
211+
}
212+
213+
const addVideoFile = (path) => {
214+
return fetch(path)
215+
.then(response => response.arrayBuffer())
216+
.then(buffer => {
217+
return ipfs.files.add({
218+
path,
219+
content: ipfs.types.Buffer.from(buffer)
220+
})
221+
})
222+
.then(result => result.pop().hash)
223+
}
224+
225+
const series = (promiseFactories) => {
226+
let result = Promise.resolve()
227+
228+
promiseFactories.forEach((promiseFactory) => {
229+
result = result.then(promiseFactory)
230+
})
231+
232+
return result
233+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
"ipfs": "^0.27.7",
21+
"ipfs-unixfs": "^0.1.14",
22+
"through": "^2.3.8",
23+
"videostream": "^2.4.2"
24+
}
25+
}
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+
}

0 commit comments

Comments
 (0)