Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
victorb committed Sep 18, 2017
0 parents commit b3898c2
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
!.gitkeep
node_modules/
package-lock.json
build/
80 changes: 80 additions & 0 deletions index.html
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<style>
body {
background-color: #03A678;
color: white;
}
body, html, h1, h3, p {
margin: 0px;
padding: 0px;
font-family: Verdana, sanf-serif;
}

h1, h3 {
text-align: center;
margin-bottom: 10px;
}

h1 {
margin-top: 10px;
}

a {
text-decoration: none;
color: white;
padding-bottom: 1px;
border-bottom: 2px solid white;
}

p {
line-height: 25px;
font-size: 16px;
}

#video {
display: none;
}

canvas {
background-color: white;
box-shadow: 1px 1px 1px 1px rgba(0,0,0,0.1);
border-radius: 50%;
margin: 10px;
}

.footer {
position: absolute;
padding: 10px;
bottom: 0px;
left: 0px;
right: 0px;
text-align: center;
}
</style>
</head>
<body>
<h1>Resort</h1>
<h3>
A experiment with showing live video feeds with
<a href="https://ipfs.io/" target="_blank">IPFS</a>
and
<a href="https://libp2p.io/" target="_blank">libp2p</a>
</h3>
<div id="feeds"></div>
<video id="video" width="128" height="128" autoplay></video>
<script src="build/index.js"></script>
<div class="footer">
<p>The source code for this experiment can be found here:
<a href="https://github.com/victorbjelkholm/resort">https://github.com/victorbjelkholm/resort</a></p>
<p>
If you can only see yourself, ask a friend to go to the same URL, or open
up the same URL in a new, incognito window.
</p>
</div>
</body>
</html>
31 changes: 31 additions & 0 deletions package.json
@@ -0,0 +1,31 @@
{
"name": "ipfs-resort",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"browserify": "^14.4.0",
"externs-generator": "^0.3.2"
},
"scripts": {
"build": "browserify -t [ babelify ] script-libp2p.js -o build/index.js",
"dev": "watchify -t [ babelify ] script-libp2p.js -o build/index.js"
},
"devDependencies": {
"babelify": "^7.3.0",
"libp2p": "^0.12.4",
"libp2p-multiplex": "^0.5.0",
"libp2p-secio": "^0.8.1",
"libp2p-webrtc-star": "^0.13.2",
"libp2p-websockets": "^0.10.1",
"multiaddr": "^3.0.1",
"peer-book": "^0.5.1",
"peer-id": "^0.10.1",
"peer-info": "^0.11.0",
"pull-pushable": "^2.1.1",
"pull-stream": "^3.6.1",
"pull-stream-to-stream": "^1.3.4",
"typedarray-to-buffer": "^3.1.2",
"watchify": "^3.9.0"
}
}
19 changes: 19 additions & 0 deletions readme.md
@@ -0,0 +1,19 @@
# Resort - WebCam over IPFS

## Install & Run

- `npm install`
- `npm run dev`
- Open a webserver in the root directory
- Navigate to page and accept sharing webcam
- Open a new incognito window with same page
- Enjoy seeing yourself, TWICE!


## TODO

- Layout
- Timeout
- Use IPFS to send hash of image instead of image itself
- Measure latency
- Debug output
212 changes: 212 additions & 0 deletions script-libp2p.js
@@ -0,0 +1,212 @@
// This version uses libp2p only and sends the data directly to peers
// - Works great for low amount of peers and requires less abstractions
// - However, large images not possible to send
// - Peers can not help to "seed" each others image, making it less efficient
const WS = require('libp2p-websockets')
const WebRTCStar = require('libp2p-webrtc-star')
const Multiplex = require('libp2p-multiplex')
const SECIO = require('libp2p-secio')
const libp2p = require('libp2p')

const PeerInfo = require('peer-info')
const PeerId = require('peer-id')
const PeerBook = require('peer-book')
const Multiaddr = require('multiaddr')
const toBuffer = require('typedarray-to-buffer')
const pull = require('pull-stream')
const pushable = require('pull-pushable')

class Node extends libp2p {
constructor (peerInfo, peerBook, options) {
options = options || {}
const wstar = new WebRTCStar()

const modules = {
transport: [new WS(), wstar],
connection: {
muxer: [Multiplex],
crypto: [SECIO]
},
discovery: [wstar.discovery]
}

super(modules, peerInfo, peerBook, options)
}
}

// CONSTANTS
// size of webcam video feed
const SIZES = {
width: 128,
height: 128
}
// Delay for sending
const SEND_EACH_MS = 100
// Protocol for our application
const PROTOCOL = '/resort/0.1.0'
// Which signal server to use, overwrite default while in dev
// const SIGNAL_SERVER = '/ip4/127.0.0.1/ws/p2p-webrtc-star/ipfs/:peer-id'
const SIGNAL_SERVER = '/dns4/star-signal.cloud.ipfs.team/wss/p2p-webrtc-star/ipfs/:peer-id'

// Create and start a libp2p Peer to use
const startOwnPeer = (callback) => {
PeerId.create((err, peerId) => {
if (err) callback(err)
const peerInfo = new PeerInfo(peerId)
const peerBook = new PeerBook()
const mh1 = Multiaddr(SIGNAL_SERVER.replace(':peer-id', peerId.toB58String()))
peerInfo.multiaddrs.add(mh1)
const node = new Node(peerInfo, peerBook, {
webRTCStar: true
})
node.start((err) => {
if (err) callback(err)
callback(null, node)
})
})
}
// Mutable global of all open connections, containing pushable pull-stream with
// Peer ID as key
const _OPEN_CONNECTIONS = {}

const listenAndConnectToPeers = (node) => {
node.on('peer:discovery', (peer) => {
const id = peer.id._idB58String
if (_OPEN_CONNECTIONS[id] === undefined) {
node.dial(peer, PROTOCOL, (err, conn) => {
// Ignore errors, as not everyone supports our protocol
if (err) return
// Create a pushable pull-stream that we can write to
const p = pushable()
pull(p, conn)
// Assign the pull-stream to our global
_OPEN_CONNECTIONS[id] = p
// Create a canvas for this peer
createCanvas(id)
})
} else {
console.log('Already connected to peer', id)
}
})
// When connection to a peer closes, remove the canvas
node.swarm.on('peer-mux-closed', (peer) => {
removeCanvas(peer.id._idB58String)
})
// TODO maybe should listen for peer errors as well
// TODO should setup a check and see when we last received data, if more than
// X seconds, kill feed
}

// Mutable global with references to all canvases we have created
const _CANVASES = {}

const createCanvas = (id) => {
if (id === undefined) throw new Error('ID needs to be defined')
if (_CANVASES[id] !== undefined) throw new Error('Already had a canvas for ID')

const canvas = document.createElement('canvas')
canvas.setAttribute('width', SIZES.width)
canvas.setAttribute('height', SIZES.height)
const ctx = canvas.getContext('2d')
_CANVASES[id] = ctx

// Write some text on the canvas while we get the first frame
ctx.font='15px Verdana'
ctx.fillText('Loading video', 15, 70)

document.getElementById('feeds').append(canvas)
}
const getCanvas = (id) => {
if (id === undefined) throw new Error('ID needs to be defined')

return _CANVASES[id]
}
const removeCanvas = (id) => {
if (id === undefined) throw new Error('ID needs to be defined')

_CANVASES[id].canvas.remove()
}

window.addEventListener('load', () => {
// Our feed from the camera
const video = document.getElementById('video');

startOwnPeer((err, node) => {
if (err) throw err
const myID = node.peerInfo.id._idB58String
console.log('Your ID: ' + myID)
createCanvas(myID)

listenAndConnectToPeers(node)
setInterval(() => {
const mhs = node.peerBook.getAllArray()
console.log('Connected to ' + mhs.length + ' other peers')
}, 2000)

// Handle incoming connections from peers
// TODO get peer id together with first data somehow?
// Right now we cannot get a live feed until getPeerInfo returns
node.handle(PROTOCOL, (protocol, conn) => {
console.log('Incoming connection')
let peerID
conn.getPeerInfo((err, info) => {
if (err) throw err
peerID = info.id._idB58String
})
pull(
conn,
pull.through((data) => {
// getPeerInfo might not have given us the Peer ID yet...
if (peerID !== undefined) {
const canvas = getCanvas(peerID)
// We might not created the canvas yet...
if (canvas !== undefined) {
const dataToRender = new ImageData(new Uint8ClampedArray(data), SIZES.width, SIZES.height)
canvas.putImageData(dataToRender, 0, 0);
}
}
}),
pull.collect((err) => {
if (err) throw err
})
)
})
// Check if webcam is supported
// TODO provide fallback if it isn't
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
// Request webcam permissions
navigator.mediaDevices.getUserMedia({ video: true }).then(function (stream) {
// Write webcam data to video element
video.src = window.URL.createObjectURL(stream)
video.play()
})
}

function drawAndSend() {
// If we don't have our own canvas yet, is because camera has yet to init
const myCanvas = getCanvas(myID)
if (myCanvas !== undefined) {

// Write video feed into canvas
myCanvas.drawImage(video, 0, 0, SIZES.width, SIZES.height)

// Get image data from canvas
const data = myCanvas.getImageData(0, 0, SIZES.width, SIZES.height)

// Convert data into buffer and send to each open connection
const bufferToWrite = toBuffer(data.data)

// For each open connection we have
Object.keys(_OPEN_CONNECTIONS).forEach((id) => {
const stream = _OPEN_CONNECTIONS[id]
// Write image data to the pull-stream
stream.push(bufferToWrite)
})
}
// Repeat this as fast as possible but with delay of 100ms
setTimeout(drawAndSend, SEND_EACH_MS)
}
// Start recursive function
drawAndSend()
})
})

0 comments on commit b3898c2

Please sign in to comment.