This repository has been archived by the owner on Feb 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: Add browser example for ReadableStreams
feat: Allows for byte offsets when using ipfs.files.cat and friends to request slices of files
- Loading branch information
1 parent
93d2bf5
commit 9ff4b79
Showing
12 changed files
with
443 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Streaming video from IPFS using ReadableStreams | ||
|
||
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. | ||
|
||
Take a look at [`index.js`](./index.js) to see a working example. | ||
|
||
## Running the demo | ||
|
||
In this directory: | ||
|
||
``` | ||
$ npm install | ||
$ npm start | ||
``` | ||
|
||
Then open [http://localhost:8888](http://localhost:8888) in your browser. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/> | ||
<title><%= htmlWebpackPlugin.options.title %></title> | ||
<style type="text/css"> | ||
|
||
body { | ||
margin: 0; | ||
padding: 0; | ||
} | ||
|
||
#container { | ||
display: flex; | ||
height: 100vh; | ||
} | ||
|
||
pre { | ||
flex-grow: 2; | ||
padding: 10px; | ||
height: calc(100vh - 45px); | ||
overflow: auto; | ||
} | ||
|
||
#form-wrapper { | ||
padding: 20px; | ||
} | ||
|
||
form { | ||
padding-bottom: 10px; | ||
display: flex; | ||
} | ||
|
||
#hash { | ||
display: inline-block; | ||
margin: 0 10px 10px 0; | ||
font-size: 16px; | ||
flex-grow: 2; | ||
padding: 5px; | ||
} | ||
|
||
button { | ||
display: inline-block; | ||
font-size: 16px; | ||
height: 32px; | ||
} | ||
|
||
video { | ||
max-width: 50vw; | ||
} | ||
|
||
</style> | ||
</head> | ||
<body> | ||
<div id="container" ondrop="dropHandler(event)" ondragover="dragOverHandler(event)"> | ||
<div id="form-wrapper"> | ||
<form> | ||
<input type="text" id="hash" placeholder="Hash" disabled /> | ||
<button id="gobutton" disabled>Go!</button> | ||
</form> | ||
<video id="video" controls></video> | ||
</div> | ||
<pre id="output" style="display: inline-block"></pre> | ||
</div> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
'use strict' | ||
|
||
/* eslint-env browser */ | ||
|
||
const Ipfs = require('../../') | ||
const videoStream = require('videostream') | ||
const ipfs = new Ipfs({ repo: 'ipfs-' + Math.random() }) | ||
const { | ||
dragDrop, | ||
statusMessages, | ||
createVideoElement, | ||
log | ||
} = require('./utils') | ||
|
||
log('IPFS: Initialising') | ||
|
||
const timeouts = [] | ||
|
||
ipfs.on('ready', () => { | ||
// Set up event listeners on the <video> element from index.html | ||
const videoElement = createVideoElement() | ||
const hashInput = document.getElementById('hash') | ||
const goButton = document.getElementById('gobutton') | ||
let stream | ||
|
||
goButton.onclick = function (event) { | ||
event.preventDefault() | ||
|
||
log(`IPFS: Playing ${hashInput.value.trim()}`) | ||
|
||
// Set up the video stream an attach it to our <video> element | ||
videoStream({ | ||
createReadStream: function createReadStream (opts) { | ||
const start = opts.start | ||
|
||
// The videostream library does not always pass an end byte but when | ||
// it does, it wants bytes between start & end inclusive. | ||
// catReadableStream returns the bytes exclusive so increment the end | ||
// byte if it's been requested | ||
const end = opts.end ? start + opts.end + 1 : undefined | ||
|
||
log(`Stream: Asked for data starting at byte ${start} and ending at byte ${end}`) | ||
|
||
// If we've streamed before, clean up the existing stream | ||
if (stream && stream.destroy) { | ||
stream.destroy() | ||
} | ||
|
||
// This stream will contain the requested bytes | ||
stream = ipfs.files.catReadableStream(hashInput.value.trim(), { | ||
offset: start, | ||
length: end && end - start | ||
}) | ||
|
||
// Log error messages | ||
stream.on('error', (error) => log(error)) | ||
|
||
if (start === 0) { | ||
// Show the user some messages while we wait for the data stream to start | ||
statusMessages(stream, log) | ||
} | ||
|
||
return stream | ||
} | ||
}, videoElement) | ||
} | ||
|
||
// Allow adding files to IPFS via drag and drop | ||
dragDrop(ipfs, log) | ||
|
||
log('IPFS: Ready') | ||
log('IPFS: Drop an .mp4 file into this window to add a file') | ||
log('IPFS: Then press the "Go!" button to start playing a video') | ||
|
||
hashInput.disabled = false | ||
goButton.disabled = false | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "browser-videostream", | ||
"version": "1.0.0", | ||
"description": "", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"build": "webpack", | ||
"start": "npm run build && http-server dist -a 127.0.0.1 -p 8888" | ||
}, | ||
"author": "", | ||
"license": "ISC", | ||
"devDependencies": { | ||
"html-webpack-plugin": "^2.30.1", | ||
"http-server": "^0.11.1", | ||
"uglifyjs-webpack-plugin": "^1.2.0", | ||
"webpack": "^3.11.0" | ||
}, | ||
"dependencies": { | ||
"videostream": "^2.4.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
const log = (line) => { | ||
const output = document.getElementById('output') | ||
let message | ||
|
||
if (line.message) { | ||
message = `Error: ${line.message.toString()}` | ||
} else { | ||
message = line | ||
} | ||
|
||
if (message) { | ||
const node = document.createTextNode(`${message}\r\n`) | ||
output.appendChild(node) | ||
|
||
output.scrollTop = output.offsetHeight | ||
|
||
return node | ||
} | ||
} | ||
|
||
const dragDrop = (ipfs) => { | ||
const container = document.querySelector('#container') | ||
|
||
container.ondragover = (event) => { | ||
event.preventDefault() | ||
} | ||
|
||
container.ondrop = (event) => { | ||
event.preventDefault() | ||
|
||
for (let i = 0; i < event.dataTransfer.items.length; i++) { | ||
const item = event.dataTransfer.items[i] | ||
|
||
if (item.kind !== 'file') { | ||
continue | ||
} | ||
|
||
const file = item.getAsFile() | ||
|
||
const progress = log(`IPFS: Adding ${file.name} 0%`) | ||
|
||
const reader = new window.FileReader() | ||
reader.onload = (event) => { | ||
ipfs.files.add({ | ||
path: file.name, | ||
content: ipfs.types.Buffer.from(event.target.result) | ||
}, { | ||
progress: (addedBytes) => { | ||
progress.textContent = `IPFS: Adding ${file.name} ${parseInt((addedBytes / file.size) * 100)}%\r\n` | ||
} | ||
}, (error, added) => { | ||
if (error) { | ||
return log(error) | ||
} | ||
|
||
const hash = added[0].hash | ||
|
||
log(`IPFS: Added ${hash}`) | ||
|
||
document.querySelector('#hash').value = hash | ||
}) | ||
} | ||
reader.readAsArrayBuffer(file) | ||
} | ||
|
||
if (event.dataTransfer.items && event.dataTransfer.items.clear) { | ||
event.dataTransfer.items.clear(); | ||
} | ||
|
||
if (event.dataTransfer.clearData) { | ||
event.dataTransfer.clearData(); | ||
} | ||
} | ||
} | ||
|
||
module.exports.statusMessages = (stream) => { | ||
let time = 0 | ||
const timeouts = [ | ||
'Stream: Still loading data from IPFS...', | ||
'Stream: This can take a while depending on content availability', | ||
'Stream: Hopefully not long now', | ||
'Stream: *Whistles absentmindedly*', | ||
'Stream: *Taps foot*', | ||
'Stream: *Looks at watch*', | ||
'Stream: *Stares at floor*', | ||
'Stream: *Checks phone*', | ||
'Stream: *Stares at ceiling*', | ||
'Stream: Got anything nice planned for the weekend?' | ||
].map(message => setTimeout(() => log(message), time += 5000)) | ||
|
||
stream.once('data', () => { | ||
log('Stream: Started receiving data') | ||
timeouts.forEach(clearTimeout) | ||
}) | ||
stream.once('error', () => { | ||
timeouts.forEach(clearTimeout) | ||
}) | ||
} | ||
|
||
const createVideoElement = () => { | ||
const videoElement = document.getElementById('video') | ||
videoElement.addEventListener('loadedmetadata', () => { | ||
videoElement.play() | ||
.catch(log) | ||
}) | ||
|
||
const events = [ | ||
'playing', | ||
'waiting', | ||
'seeking', | ||
'seeked', | ||
'ended', | ||
'loadedmetadata', | ||
'loadeddata', | ||
'canplay', | ||
'canplaythrough', | ||
'durationchange', | ||
'play', | ||
'pause', | ||
'suspend', | ||
'emptied', | ||
'stalled', | ||
'error', | ||
'abort' | ||
] | ||
events.forEach(event => { | ||
videoElement.addEventListener(event, () => { | ||
log(`Video: ${event}`) | ||
}) | ||
}) | ||
|
||
videoElement.addEventListener('error', () => { | ||
log(videoElement.error) | ||
}) | ||
|
||
return videoElement | ||
} | ||
|
||
module.exports.log = log | ||
module.exports.dragDrop = dragDrop | ||
module.exports.createVideoElement = createVideoElement |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
'use strict' | ||
|
||
const path = require('path') | ||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin') | ||
const HtmlWebpackPlugin = require('html-webpack-plugin') | ||
|
||
module.exports = { | ||
devtool: 'source-map', | ||
entry: [ | ||
'./index.js' | ||
], | ||
plugins: [ | ||
new UglifyJsPlugin({ | ||
sourceMap: true, | ||
uglifyOptions: { | ||
mangle: false, | ||
compress: false | ||
} | ||
}), | ||
new HtmlWebpackPlugin({ | ||
title: 'IPFS Videostream example', | ||
template: 'index.html' | ||
}) | ||
], | ||
output: { | ||
path: path.join(__dirname, 'dist'), | ||
filename: 'bundle.js' | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.