├── client ├── index.js └── peer-connect-client.js ├── test ├── test.css ├── styles.css ├── server.js ├── testClient.js ├── demo.js ├── test.js └── index.html ├── .gitignore ├── LICENSE ├── package.json ├── my.conf.js ├── server ├── videoConnect.js ├── peerConnect.js └── index.js └── README.md /client/index.js: -------------------------------------------------------------------------------- 1 | const PeerConnectClient = require('peer-connect-client'); 2 | PeerConnectClient(); 3 | -------------------------------------------------------------------------------- /test/test.css: -------------------------------------------------------------------------------- 1 | #peer-connect-id { 2 | height: 300px; 3 | background-image: url(/assets/peerConnect.png); 4 | border: 2px solid #749099; 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .eslintrc.js 3 | node_modules 4 | .DS_Store 5 | .DS_Store? 6 | *.log 7 | npm-debug.log* 8 | client/bundle.js 9 | assets 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:900|Raleway:300,400'); 2 | 3 | body { 4 | background: url(/assets/circles-dark.png) repeat; 5 | font-family: 'Open Sans', sans-serif; 6 | } 7 | 8 | .title { 9 | margin-bottom: 20px; 10 | font-size: 60px; 11 | font-weight: 300; 12 | font-family: 'Raleway', sans-serif; 13 | text-align: center; 14 | color: #9F2738; 15 | } 16 | 17 | .bold { 18 | font-weight: 900; 19 | } 20 | 21 | .image-container-inner { 22 | overflow: hidden; 23 | } 24 | 25 | img { 26 | position: relative; 27 | left: 50%; 28 | height: 300px; 29 | transform: translateX(-50%); 30 | } 31 | 32 | main { 33 | text-align: center; 34 | max-width: 920px; 35 | margin: 0 auto; 36 | } 37 | 38 | #image-container { 39 | display: grid; 40 | grid-template-columns: 300px 300px 300px; 41 | grid-gap: 10px; 42 | margin-top: 10px; 43 | } 44 | 45 | #video { 46 | max-width: 500px; 47 | } 48 | 49 | .peer-connect-class { 50 | height: 300px; 51 | background: url(/assets/peerConnect.png); 52 | border: 2px solid #9F2738; 53 | } 54 | 55 | .peerConnectCamelCase { 56 | height: 300px; 57 | background: url(/assets/peerConnect.png); 58 | border: 2px solid #8F8F91; 59 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peer-connect-old", 3 | "version": "1.0.0", 4 | "description": "P2P CDN", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon server.js", 8 | "test": "karma start my.conf.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/PeerConnect/peer-connect.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/PeerConnect/peer-connect/issues" 18 | }, 19 | "homepage": "https://github.com/PeerConnect/peer-connect#readme", 20 | "dependencies": { 21 | "create-torrent": "^3.29.2", 22 | "express": "^4.16.2", 23 | "node-fetch": "^1.7.3", 24 | "parse-torrent": "^5.8.3", 25 | "path": "^0.12.7", 26 | "peer-connect": "^0.1.2", 27 | "peer-connect-client": "^0.1.3", 28 | "peer-connect-server": "^0.1.1", 29 | "simple-peer": "^8.2.0", 30 | "socket.io": "^2.0.4", 31 | "stream-http": "^2.8.0", 32 | "webtorrent": "^0.98.20", 33 | "wrtc": "0.0.65", 34 | "socket.io-client": "^2.0.4" 35 | }, 36 | "devDependencies": { 37 | "eslint-config-airbnb": "^16.1.0", 38 | "eslint-plugin-jsx-a11y": "^6.0.3", 39 | "eslint-plugin-react": "^7.5.1", 40 | "karma": "2.0.0", 41 | "chai": "4.1.2", 42 | "karma-chai": "0.1.0", 43 | "karma-chrome-launcher": "2.2.0", 44 | "karma-mocha": "1.3.0", 45 | "eslint": "^4.15.0", 46 | "eslint-config-airbnb-base": "^12.1.0", 47 | "eslint-plugin-import": "^2.8.0", 48 | "mocha": "^4.1.0", 49 | "nodemon": "^1.14.7", 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | // const PeerConnectServer = require('./server/index.js') 4 | const PeerConnectServer = require('peer-connect-server') 5 | // App setup 6 | const PORT = process.env.PORT || 8080; 7 | const app = express(); 8 | const server = app.listen(PORT, () => 9 | console.log(`App listening on port ${PORT}...`) 10 | ); 11 | 12 | app.use(express.static(path.join(__dirname, "/"))); 13 | 14 | // Allow for cross origin resource sharing 15 | app.use((req, res, next) => { 16 | res.header("Access-Control-Allow-Origin", "*"); 17 | res.header( 18 | "Access-Control-Allow-Headers", 19 | "Origin, X-Requested-With, Content-Type, Accept" 20 | ); 21 | next(); 22 | }); 23 | 24 | // PeerConnect configuration 25 | const peerConfig = { 26 | // how many peers must be connected before loading assets from peers 27 | // if threshold = 3, fourth client will load from peers 28 | threshold: 1, 29 | //load images p2p 30 | peerImages: true, 31 | //load videos p2p 32 | peerVideos: false, 33 | // asset file formats to exclude from peers 34 | excludeFormats: ['gif'], 35 | // load images above the fold from server if foldLoading: true 36 | foldLoading: true, 37 | // toggle geolocation for pairing peers 38 | geolocate: true, 39 | // route for video assets 40 | videoRoute: './assets/videos', 41 | //where you want to create torrent files 42 | torrentRoute: './assets', 43 | //domain name 44 | domainName: 'https://webseed.btorrent.xyz', 45 | }; 46 | 47 | PeerConnectServer(server, app, peerConfig); 48 | -------------------------------------------------------------------------------- /test/testClient.js: -------------------------------------------------------------------------------- 1 | function loadVideosFromServer(videoArray) { 2 | videoArray.forEach(element => { 3 | let source = element.dataset.src; 4 | setServerAsset(source); 5 | }); 6 | } 7 | 8 | function setServerAsset(imageSource) { 9 | document.querySelector(`[data-src='${imageSource}']`).setAttribute('src', `${imageSource}`); 10 | } 11 | 12 | function checkForImageError(imageArray) { 13 | for (let i = 0; i < imageArray.length; i++) { 14 | let source = imageArray[i].dataset.src; 15 | imageArray[i].onerror = function () { 16 | setServerAsset(source); 17 | } 18 | } 19 | } 20 | 21 | function checkForMobile() { 22 | testExp = new RegExp('Android|webOS|iPhone|iPad|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile', 'i'); 23 | return !!testExp.test(navigator.userAgent); 24 | } 25 | 26 | function isElementInViewport(el) { 27 | const rect = el.getBoundingClientRect(); 28 | return ( 29 | rect.top >= 0 && 30 | rect.left >= 0 && 31 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 32 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 33 | ); 34 | } 35 | 36 | function getImageData(image, selector, bgProperty) { 37 | let canvas = document.createElement('canvas'); 38 | let context = canvas.getContext('2d'); 39 | let img = bgProperty ? new Image() : image; 40 | let type = bgProperty ? '' : getImageType(image); 41 | if (bgProperty) img.src = image; 42 | context.canvas.width = img.width; 43 | context.canvas.height = img.height; 44 | context.drawImage(img, 0, 0, img.width, img.height); 45 | if (bgProperty) setBackgroundImage(selector, bgProperty, canvas.toDataURL()); 46 | return canvas.toDataURL(`image/${type}`); 47 | } 48 | 49 | function getImageType(image) { 50 | const imageSrc = image.dataset.src; 51 | const regex = /(?:\.([^.]+))?$/; 52 | return regex.exec(imageSrc)[1]; 53 | } 54 | 55 | function setImageHeightsToSend(imageArray) { 56 | return imageArray.map(imageNode => imageNode.height); 57 | } -------------------------------------------------------------------------------- /test/demo.js: -------------------------------------------------------------------------------- 1 | const demoFunctions = { 2 | browserOpenTime: new Date(), 3 | currentTime: new Date(), 4 | assetsFromServer: function () { 5 | document.getElementsByClassName('loading_gif')[0].style.display = 'none'; 6 | document.getElementById('downloaded_from').innerHTML = 'Assets downloaded from the SERVER!'; 7 | document.getElementById('downloaded_from').style.display = ''; 8 | }, 9 | assetsFromPeerMessage: function () { 10 | document.getElementById('downloaded_from').innerHTML = 'Assets downloaded from a PEER!'; 11 | }, 12 | assetsFromPeer: function (initiatorData) { 13 | document.getElementsByClassName('loading_gif')[0].style.display = 'none'; 14 | this.assetsFromPeerMessage(); 15 | document.getElementById('downloaded_from').style.display = ''; 16 | document.getElementById('report').style.display = ''; 17 | document.getElementById('peer_info').style.display = ''; 18 | if (initiatorData.location) { 19 | const { location } = initiatorData; 20 | document.getElementById('peer_info').innerHTML += 21 | `
* Received data from ${location.city}, ${location.regionCode}, ${location.country} ${location.zipCode};`; 22 | } 23 | }, 24 | sentDataToPeerLocation: function (peerLocation) { 25 | // location data of peer to render on page for demo 26 | document.getElementById('peer_info').style.display = ''; 27 | if (peerLocation) { 28 | document.getElementById('peer_info').innerHTML += 29 | `
* Sent data to ${peerLocation.city}, ${peerLocation.regionCode}, ${peerLocation.country} ${peerLocation.zipCode};`; 30 | } 31 | }, 32 | appendTime: function (imageIndex, currentTime) { 33 | document.getElementById(imageIndex).parentNode.appendChild(document.createTextNode(`${new Date() - currentTime} ms`)); 34 | }, 35 | reportTime: function (currentOrTotal, domId) { 36 | // function that reports time to DOM 37 | const time = new Date(); 38 | document.getElementById(domId).innerHTML += `${time - currentOrTotal} ms`; 39 | currentTime = new Date(); 40 | }, 41 | timeTotalFromServer: function (browserOpenTime) { 42 | document.getElementById('time_total_from_server').innerHTML = `Time it took to load from server: ${new Date() - browserOpenTime} ms `; 43 | } 44 | } -------------------------------------------------------------------------------- /my.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Jan 23 2018 11:32:49 GMT-0800 (PST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'chai'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | // '../index.html', 19 | // {pattern: '**/*.html', watched: true, served: true, included: false}, 20 | './test/*.js' 21 | ], 22 | 23 | proxies : { 24 | // '/': 'http://localhost:8080' 25 | }, 26 | 27 | 28 | // list of files / patterns to exclude 29 | exclude: [ 30 | ], 31 | 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: { 36 | // './client/sockets.js': [ 'browserify' ] 37 | // "**/*.html": [] 38 | }, 39 | 40 | browserify: { 41 | debug: true, 42 | // transform: [ 'simple-peer', 'parse-torrent', 'stream-http', 'webtorrent', 'should' ] 43 | transform: [] 44 | }, 45 | 46 | 47 | // test results reporter to use 48 | // possible values: 'dots', 'progress' 49 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 50 | reporters: ['progress'], 51 | 52 | 53 | // web server port 54 | port: 9876, 55 | 56 | 57 | // enable / disable colors in the output (reporters and logs) 58 | colors: true, 59 | 60 | 61 | // level of logging 62 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 63 | logLevel: config.LOG_INFO, 64 | 65 | 66 | // enable / disable watching file and executing tests whenever any file changes 67 | autoWatch: true, 68 | 69 | 70 | // start these browsers 71 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 72 | browsers: ['Chrome'], 73 | 74 | 75 | // Continuous Integration mode 76 | // if true, Karma captures browsers, runs the tests and exits 77 | singleRun: false, 78 | 79 | // Concurrency level 80 | // how many browser should be started simultaneous 81 | concurrency: Infinity 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /server/videoConnect.js: -------------------------------------------------------------------------------- 1 | module.exports = function (peerConfig, app) { 2 | const createTorrent = require('create-torrent'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const videoRoute = peerConfig.videoRoute; 6 | const torrentRoute = peerConfig.torrentRoute; 7 | const domainName = peerConfig.domainName; 8 | 9 | 10 | fs.readdir(path.join(__dirname, '../', videoRoute), (err, files) => { 11 | if (err) { 12 | console.log(err); 13 | } 14 | 15 | //create routes for each mp4 file to serve as webseeds 16 | if (files.length) { 17 | files.forEach(file => { 18 | // console.log(file); 19 | app.get(`/video/${file}`, (req, res) => { 20 | res.sendFile(path.join(__dirname, '../', route, file)); 21 | }); 22 | }); 23 | } 24 | }); 25 | 26 | 27 | //if torrent folder already exists, just create routes 28 | if (fs.existsSync(`${torrentRoute}/torrent`)) { 29 | fs.readdir(path.join(__dirname,'../', videoRoute), (err, files) => { 30 | if (err) { 31 | console.log(err); 32 | } 33 | 34 | //loop through video files and create torrent routes that send torrent files 35 | files.forEach(file => { 36 | app.get(`/torrent/${file.slice(0, -4)}.torrent`, (req, res) => { 37 | res.sendFile(path.join(__dirname,'../', `${torrentRoute}/torrent`, `${file.slice(0, -4)}.torrent`)); 38 | }); 39 | }); 40 | }); 41 | return 42 | } 43 | 44 | //make torrent directory 45 | fs.mkdir(`${torrentRoute}/torrent`); 46 | 47 | fs.readdir(path.join(__dirname,'../', videoRoute), (err, files) => { 48 | if (err) { 49 | console.log(err); 50 | } 51 | 52 | files.forEach(file => { 53 | //this is for actual 54 | //create torrents with the mp4 links as webseed 55 | // createTorrent((path.join(__dirname,'../', videoRoute, file)), { urlList: [`${domainName}/video/${file}`] }, (err, torrent) => { 56 | 57 | //this is for test 58 | createTorrent((path.join(__dirname,'../', videoRoute, file)), { urlList: [`${domainName}/${file}`] }, (err, torrent) => { 59 | fs.writeFile(__dirname,'../' + `/assets/torrent/${file.slice(0 , -4)}.torrent`, torrent); 60 | }); 61 | //create routes to serve torrent files according to name 62 | app.get(`/torrent/${file.slice(0, -4)}.torrent`, (req, res) => { 63 | res.sendFile(path.join(__dirname,'../', `${torrentRoute}/torrent`, `${file.slice(0, -4)}.torrent`)); 64 | }); 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | let assert = chai.assert; 2 | let expect = chai.expect; 3 | 4 | describe('loading and manipulating dom assets', () => { 5 | before(() => { 6 | const fixture = '' + 7 | '' + 8 | '' + 9 | ''; 10 | document.body.insertAdjacentHTML( 11 | 'afterbegin', 12 | fixture); 13 | }); 14 | 15 | it('expected one video data-src to be available in src', () => { 16 | const video = document.getElementById('video'); 17 | setServerAsset(video.dataset.src); 18 | expect(video.src).to.equal('http://localhost:9876/assets/videos/agitation-new-zealand-4k.mp4'); 19 | }); 20 | 21 | it('expected all video src to load from server', () => { 22 | const videoArray = Object.values(document.getElementsByTagName('video')); 23 | loadVideosFromServer(videoArray); 24 | expect(videoArray[0].src).to.equal('http://localhost:9876/assets/videos/agitation-new-zealand-4k.mp4'); 25 | expect(videoArray[1].src).to.equal('http://localhost:9876/assets/videos/yosemite-hd.mp4'); 26 | }); 27 | 28 | it('expected all images to have an onerror function', () => { 29 | const imageArray = Object.values(document.getElementsByTagName('img')); 30 | checkForImageError(imageArray); 31 | expect(imageArray[0].onerror).to.be.a('function'); 32 | }); 33 | 34 | it('expected to return false for user on mobile', () => { 35 | let mobile = checkForMobile(); 36 | expect(mobile).to.be.false; 37 | }); 38 | 39 | it('expected first image element to return true for viewport', () => { 40 | const imageArray = Object.values(document.getElementsByTagName('img')); 41 | let inViewport = isElementInViewport(imageArray[0]); 42 | expect(inViewport).to.be.true; 43 | }); 44 | 45 | it('expected image src to image data to return a string', () => { 46 | const imageArray = Object.values(document.getElementsByTagName('img')); 47 | let data = getImageData(imageArray[0]); 48 | expect(data).to.be.a('string'); 49 | }); 50 | 51 | it('expected to get png image type', () => { 52 | const imageArray = Object.values(document.getElementsByTagName('img')); 53 | const type = getImageType(imageArray[1]); 54 | expect(type).to.equal('png'); 55 | }); 56 | 57 | it('expected to get heights of 300 for image 1, 250 for image 2', () => { 58 | const imageArray = Object.values(document.getElementsByTagName('img')); 59 | const imageHeights = setImageHeightsToSend(imageArray); 60 | expect(imageHeights[0]).to.equal(300); 61 | expect(imageHeights[1]).to.equal(250); 62 | }); 63 | }); -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PeerConnect 8 | 9 | 10 | 11 | 12 | 13 | 14 |
PeerConnect
15 |
16 | 17 |
18 | 19 |
20 |
21 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 | 65 |
66 |
67 | 68 |
69 |
70 | 71 |
72 |
73 | 74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |
6 | PeerConnect 7 |
8 |

9 | 10 |

A P2P CDN Implementation

11 | 12 | [![Price](https://img.shields.io/badge/price-FREE-0098f7.svg)](https://github.com/peerconnect/peer-connect/blob/master/LICENSE) 13 | [![npm](https://img.shields.io/npm/v/peer-connect-client.svg)](https://www.npmjs.com/package/peer-connect-client) 14 | [![license](https://img.shields.io/github/license/peerconnect/peer-connect.svg)](https://github.com/peerconnect/peer-connect/) 15 | 16 | ## About 17 | PeerConnect is a proof of concept that aims to serve static assets (videos/images) over a peer to peer delivery network powered by WebRTC (images), WebTorrent (videos), and WebSockets (signaling) 18 | ### Images 19 | `PeerConnect` uses WebRTC for image P2P transfers. By using websockets, we are able to coordinate data-channel connections between two different peers. If no available peers are present, images are loaded from the server. Once a peer finishes downloading, they become an initiator for future P2P data transfers. 20 | ### Video 21 | `PeerConnect` uses WebTorrent and torrenting protocols for video P2P transfers. By utilizing the server as a webseed for videos, as more and more individuals visit the site, video streams will get progressively stronger and rely less on the initial webseed hosted on the server. 22 | ## Usage 23 | Using PeerConnect requires a script on the client end and initiation on the server. 24 | ### Setup 25 | ``` 26 | npm install --save peer-connect-client peer-connect-server 27 | ``` 28 | #### Client 29 | If using a file bundler e.g. (browserify), you can require it in your script file. 30 | ```js 31 | const PeerConnectClient = require('peer-connect-client'); 32 | PeerConnectClient(); 33 | ``` 34 | If you want to use the module without bundling, it is currently being hosted on unpkg CDN. Use it as a script in your html file. 35 | ``` 36 | https://unpkg.com/peer-connect-client@0.1.3/peer-connect-client.min.js 37 | ``` 38 | You will need to bring in socket.io as a script in your html file. 39 | ```html 40 | 41 | ``` 42 | #### Server 43 | PeerConnect utilizes Express and socket.io to coordinate WebRTC connections. In addition, in order to create webseeds, we create routes serving the video files. 44 | 45 | To use it, require PeerConnectServer from our package and pass in the Node Server instance you're using along with your PeerConnectServer configurations. In Express, you can get this instance by calling app.listen. 46 | 47 | Here's how you would use it in your server: 48 | ```js 49 | const PeerConnectServer = require('peer-connect-server'); 50 | const server = app.listen(8000); 51 | 52 | //to allow cross origin resource sharing 53 | app.use((req, res, next) => { 54 | res.header("Access-Control-Allow-Origin", "*"); 55 | res.header( 56 | "Access-Control-Allow-Headers", 57 | "Origin, X-Requested-With, Content-Type, Accept" 58 | ); 59 | next(); 60 | }); 61 | 62 | PeerConnectServer(server, app, [opts]); 63 | ``` 64 | 65 | ### Example 66 | Set src attributes in a data-src attribute for assets! 67 | ```html 68 | 69 |
70 | 71 | 72 |
73 | ``` 74 | 75 | ### Configuration 76 | It's easy to incorporate `PeerConnectServer`. Just provide us with a few details on your peerConfig object and we'll do the rest! 77 | If opts is specified to PeerConnectServer, it will override the default options (shown below). 78 | 79 | ```threshold``` - An integer threshold value to determine when to turn on P2P image sharing e.g. if threshold = 3, fourth client will load from peers 80 |
```peerImages``` - A boolean that determines whether to load images P2P 81 |
```peerVideos``` - A boolean that determines whether to load videos P2P 82 |
```excludeFormats``` - An array of string(s) that say which file formats to exclude from peers 83 |
```foldLoading``` - A boolean that determines whether to load images above the fold from the server if true 84 |
```geoLocate``` - A boolean that either uses geolocation to pair the closest peers or not 85 |
```videoRoute``` - The path to all of your video assets 86 |
```torrentRoute``` - The path where a new folder holding all of your torrent files will be created 87 |
```domainName``` - Your website url 88 | ``` 89 | { 90 | threshold: Integer // 3 91 | peerImages: Boolean // true 92 | peerVideos: Boolean // true 93 | excludeFormats: [Strings] // ['gif'] 94 | foldLoading: Boolean // false 95 | geoLocate: Boolean // true 96 | videoRoute: String // './assets/videos' 97 | torrentRoute: String // './assets' 98 | domainName: String // 'https://peerconnect.io' 99 | } 100 | ``` 101 | 102 | ## Contributing 103 | To contribute to `PeerConnect`, fork the repository and clone it to your machine then install dependencies with `npm install`. If you're interested in joining the Peer Connect team as a contributor, feel free to message one of us directly! 104 | ## Authors 105 | - Justin Ko (https://github.com/justinko43) 106 | - Mike Gutierrez (https://github.com/mikegutierrez) 107 | - Peter Lee (https://github.com/wasafune) 108 | - Jim Kang (https://github.com/jiminykbob) 109 | 110 | ## License 111 | This project is licensed under the MIT License - see the LICENSE file for details 112 | -------------------------------------------------------------------------------- /server/peerConnect.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: ["error", { "functions": false }] */ 2 | 3 | const socket = require('socket.io'); 4 | const fetch = require('node-fetch'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | // all filetypes 9 | 10 | const imageTypes = ['jpeg', 'jpg', 'png', 'gif']; 11 | 12 | 13 | function PeerConnect(config, server) { 14 | // DEFAULT CONFIGURABLES 15 | this.config = { ...config }; // eslint rules: parameters should be immutable 16 | this.config.threshold = this.config.threshold || 1; 17 | this.config.foldloading = this.config.foldLoading !== false; // default true 18 | this.config.geolocate = this.config.geolocate !== false; // defaults to true 19 | this.config.peerVideos = this.config.peerVideos !== false; //defaults to true 20 | this.config.peerImages = this.config.peerImages !== false; //defaults to true 21 | 22 | // REFERENCED CONFIGURABLES 23 | // include the inputted media types 24 | // filter out the excluded assetTypes after lowercasing excludeFormats 25 | this.config.excludeFormats = lowerCaseConfig(this.config.excludeFormats); 26 | 27 | if (!this.config.peerImages) { 28 | this.config.assetTypes = []; 29 | } else { 30 | this.config.assetTypes = imageTypes.filter(type => !this.config.excludeFormats.includes(type)); 31 | } 32 | 33 | // NON-CONFIGURABLES 34 | // Sockets setup 35 | this.io = socket(server); 36 | // Store list of all clients actively using app 37 | this.activeClients = {}; 38 | // number of clients, number of intiators 39 | this.serverStats = { 40 | numClients: 0, 41 | numInitiators: 0, 42 | hasHeights: false, 43 | imageHeights: [], 44 | }; 45 | //set up for video 46 | 47 | // server socket 48 | this.io.on('connection', (client) => { 49 | console.log(`socket connection started. ID: ${client.id}`); 50 | console.log('imageHeights is: ', this.serverStats.imageHeights); 51 | this.serverStats.numClients += 1; 52 | this.activeClients[client.id] = { 53 | id: client.id, 54 | initiator: false, 55 | offer: null, 56 | location: null, 57 | }; 58 | 59 | //fs loop for torrents 60 | if (this.config.peerVideos) { 61 | fs.readdir(path.join(__dirname, '../', `${config.torrentRoute}/torrent`), (err, files) => { 62 | if (err) { 63 | console.log(err); 64 | } 65 | files.forEach(file => { 66 | // console.log(file); 67 | client.emit('torrent', `${file}`) 68 | }); 69 | }); 70 | } else { 71 | client.emit('load_server_video'); 72 | } 73 | 74 | 75 | // creation of peers handled here 76 | if (this.config.geolocate && this.config.peerImages) { 77 | // cip is the client's ip address 78 | // if localhost use static ip 79 | this.staticIP = '45.59.229.42'; 80 | this.cip = client.client.request.headers['x-forwarded-for'] || client.client.conn.remoteAddress; 81 | if (this.cip[0] === ':') this.cip = this.staticIP; 82 | // fetch request to IP API to determine location (longitude, latitude) 83 | // save location to activeClients 84 | fetch(`http://freegeoip.net/json/${this.cip}`) 85 | .then(res => res.json()) 86 | .then((json) => { 87 | const location = { 88 | lgn: json.longitude, 89 | lat: json.latitude, 90 | city: json.city, 91 | zipCode: json.zip_code, 92 | regionCode: json.region_code, 93 | country: json.country_code, 94 | }; 95 | this.activeClients[client.id].location = location; 96 | // create base initiator if no avaliable initiator 97 | // initiators avaliable, create receiver 98 | if (this.serverStats.numInitiators < this.config.threshold) { 99 | createBaseInitiator(client, this.config); 100 | } else { 101 | createReceiver(client, this.activeClients, this.config, this.serverStats); 102 | } 103 | }) 104 | .catch((err) => { 105 | // if API fetch fails, turn of geolocate and create new initiator 106 | console.log(err); 107 | createBaseInitiator(client, this.config); 108 | }); 109 | } else { 110 | // if geolocate is off 111 | if (this.serverStats.numInitiators < this.config.threshold || !this.config.peerImages) { 112 | createBaseInitiator(client, this.config); 113 | } 114 | else if (this.serverStats.numInitiators >= this.config.threshold) { 115 | createReceiver(client, this.activeClients, this.config, this.serverStats); 116 | } 117 | } 118 | // Initiator sent offer object to server. 119 | // Store offer object to the client's respective object inside this.activeClients. 120 | // Set this client to an initiator and update this.numInitiators count. 121 | client.on('offer_to_server', (message, imageHeights, hasHeights) => { 122 | this.serverStats.numInitiators += 1; 123 | this.activeClients[client.id].initiator = true; 124 | this.activeClients[client.id].offer = message.offer; 125 | if (imageHeights && !this.serverStats.hasHeights) { 126 | this.serverStats.imageHeights = imageHeights; 127 | this.serverStats.hasHeights = hasHeights; 128 | } 129 | console.log(`numClients, numInitiators: ${this.serverStats.numClients}, ${this.serverStats.numInitiators}`); 130 | }); 131 | 132 | // Receiver sent answer object to server. 133 | // Send this answer object to the specific initiator that 134 | // provided the offer object to the receiver. 135 | client.on('answer_to_server', (message, imageSliceIndex) => { 136 | client.to(message.peerId).emit('answer_to_initiator', message.answer, this.activeClients[client.id].location, imageSliceIndex); 137 | }); 138 | // if diconnected user was an initiator, update accordingly with this.numClients as well 139 | client.on('disconnect', () => { 140 | console.log(`disconnecting ${client.id}`); 141 | if (this.activeClients[client.id].initiator) { 142 | this.serverStats.numInitiators -= 1; 143 | } 144 | delete this.activeClients[client.id]; 145 | this.serverStats.numClients -= 1; 146 | console.log(`numClients, numInitiators: ${this.serverStats.numClients}, ${this.serverStats.numInitiators}`); 147 | }); 148 | client.on('error', err => console.log(err)); 149 | }); 150 | } 151 | 152 | // create initiators after ip geolocation api call 153 | function createBaseInitiator(client, config) { 154 | client.emit('create_base_initiator', config.assetTypes, config.foldLoading, this.serverStats.hasHeights); 155 | } 156 | function createReceiver(client, activeClients, config, serverStats) { 157 | this.serverStats = serverStats; 158 | this.activeClients = activeClients; 159 | // checks if geolocate config is on 160 | if (config.geolocate) { 161 | // current client's location 162 | const clientLocation = this.activeClients[client.id].location; 163 | // placeholder for the closest peer 164 | const closestPeer = { 165 | id: '', 166 | distance: Infinity, 167 | }; 168 | // iterate through this.activeClients to find closest initiator avaliable 169 | // make that initiator unavaliable (initiator key set to false). 170 | let tempLocation = null; 171 | let tempDistance = 0; 172 | Object.values(this.activeClients).forEach((clientObj) => { 173 | if (clientObj.initiator) { 174 | tempLocation = this.activeClients[clientObj.id].location; 175 | tempDistance = distance( 176 | clientLocation.lat, 177 | clientLocation.lgn, 178 | tempLocation.lat, 179 | tempLocation.lgn, 180 | ); 181 | if (tempDistance <= closestPeer.distance) { 182 | closestPeer.id = clientObj.id; 183 | closestPeer.distance = tempDistance; 184 | } 185 | } 186 | }); 187 | const selectedInitiator = this.activeClients[closestPeer.id]; 188 | const initiatorData = { 189 | offer: selectedInitiator.offer, 190 | peerId: closestPeer.id, 191 | location: selectedInitiator.location, 192 | }; 193 | this.activeClients[closestPeer.id].initiator = false; 194 | // Updates this.numInitiators and emit to receiver and send initiator data 195 | this.serverStats.numInitiators -= 1; 196 | console.log(config.assetTypes); 197 | client.emit('create_receiver_peer', initiatorData, config.assetTypes, config.foldLoading, this.serverStats.imageHeights); 198 | } else { 199 | // loops through activeClients and randomly finds avaliable initiator 200 | const initiatorsArr = []; 201 | Object.values(this.activeClients).forEach((clientObj) => { 202 | if (clientObj.initiator) initiatorsArr.push(clientObj.id); 203 | }); 204 | const selectedInitiatorId = initiatorsArr[Math.floor(Math.random() * initiatorsArr.length)]; 205 | const initiatorData = { 206 | offer: this.activeClients[selectedInitiatorId].offer, 207 | peerId: selectedInitiatorId, 208 | }; 209 | this.activeClients[selectedInitiatorId].initiator = false; 210 | // Updates this.numInitiators and emit to receiver and send initiator data 211 | this.serverStats.numInitiators -= 1; 212 | console.log(config.assetTypes); 213 | client.emit('create_receiver_peer', initiatorData, config.assetTypes, config.foldLoading); 214 | } 215 | } 216 | 217 | // function to calculate distance using two sets of coordindates 218 | // source: https://www.geodatasource.com/developers/javascript 219 | function distance(lat1, lon1, lat2, lon2) { 220 | const radlat1 = Math.PI * (lat1 / 180); 221 | const radlat2 = Math.PI * (lat2 / 180); 222 | const theta = lon1 - lon2; 223 | const radtheta = Math.PI * (theta / 180); 224 | let dist = (Math.sin(radlat1) * Math.sin(radlat2)); 225 | dist += (Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta)); 226 | dist = Math.acos(dist); 227 | dist = (dist * 180) / Math.PI; 228 | dist = dist * 60 * 1.1515; 229 | return dist; 230 | } 231 | 232 | function declareAssetTypes(mediaTypes, typesObj) { 233 | return ( 234 | mediaTypes.reduce((includedTypes, mediaType) => includedTypes.concat(typesObj[mediaType]), []) 235 | ); 236 | } 237 | function lowerCaseConfig(arr) { 238 | return arr.map(str => str.toLowerCase()); 239 | } 240 | 241 | module.exports = PeerConnect; 242 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: ["error", { "functions": false }] */ 2 | 3 | const socket = require('socket.io'); 4 | const fetch = require('node-fetch'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const appDir = path.dirname(require.main.filename); 8 | 9 | /** 10 | * Peer Connect object 11 | * @constructor 12 | * @param {object} server - Put your server in here. 13 | * @param {object} app - Put your app in here. 14 | * @param {object} peerConfig - The config object. 15 | */ 16 | function PeerConnectServer(server, app, peerConfig) { 17 | ImageConnect(server, peerConfig); 18 | if (peerConfig.peerVideos) VideoConnect(app, peerConfig); 19 | } 20 | 21 | /** 22 | * Function that handles images 23 | */ 24 | function ImageConnect(server, peerConfig) { 25 | /** 26 | * Config object defaults to true if not specified. 27 | */ 28 | this.peerConfig = { ...peerConfig }; // eslint rules: parameters should be immutable 29 | this.peerConfig.threshold = this.peerConfig.threshold || 1; 30 | this.peerConfig.foldloading = this.peerConfig.foldLoading !== false; 31 | this.peerConfig.geolocate = this.peerConfig.geolocate !== false; 32 | this.peerConfig.peerVideos = this.peerConfig.peerVideos !== false; 33 | this.peerConfig.peerImages = this.peerConfig.peerImages !== false; 34 | 35 | const imageTypes = ['jpeg', 'jpg', 'png', 'gif']; 36 | 37 | /** Filter out the excluded assetTypes */ 38 | this.peerConfig.excludeFormats = this.peerConfig.excludeFormats 39 | .map(str => str.toLowerCase()); 40 | 41 | if (!this.peerConfig.peerImages) { 42 | this.peerConfig.assetTypes = []; 43 | } else { 44 | this.peerConfig.assetTypes = imageTypes 45 | .filter(type => !this.peerConfig.excludeFormats.includes(type)); 46 | } 47 | 48 | /** NON-CONFIGURABLES - Sockets setup */ 49 | this.io = socket(server); 50 | /** Stores list of all clients actively using app */ 51 | this.activeClients = {}; 52 | /** Information that signaling server holds */ 53 | this.serverStats = { 54 | numClients: 0, 55 | numInitiators: 0, 56 | hasHeights: false, 57 | imageHeights: [], 58 | }; 59 | 60 | /** Socket.io - 'connection' triggers on client connection */ 61 | this.io.on('connection', (client) => { 62 | // console.log(`socket connection started. ID: ${client.id}`); 63 | this.serverStats.numClients += 1; 64 | this.activeClients[client.id] = { 65 | id: client.id, 66 | initiator: false, 67 | offer: null, 68 | location: null, 69 | }; 70 | 71 | /** Fs loop for torrents */ 72 | if (this.peerConfig.peerVideos) { 73 | fs.readdir(appDir + `${peerConfig.torrentRoute.slice(1)}/torrent`, (err, files) => { 74 | if (err) { 75 | console.log(err); 76 | } 77 | files.forEach(file => { 78 | client.emit('torrent', `${file}`) 79 | }); 80 | }); 81 | } else { 82 | client.emit('load_server_video'); 83 | } 84 | 85 | /** Creation of peers handled here */ 86 | if (this.peerConfig.geolocate && this.peerConfig.peerImages) { 87 | 88 | /** Uses staticIP if localhost uses static ip */ 89 | this.staticIP = '45.59.229.42'; 90 | /** cip is the client's ip address */ 91 | this.cip = client.client.request.headers['x-forwarded-for'] || client.client.conn.remoteAddress; 92 | if (this.cip[0] === ':') this.cip = this.staticIP; 93 | 94 | /** 95 | * Fetch request to IP API to determine location (longitude, latitude) 96 | * Saves location to activeClients 97 | */ 98 | fetch(`http://freegeoip.net/json/${this.cip}`) 99 | .then(res => res.json()) 100 | .then((json) => { 101 | const location = { 102 | lgn: json.longitude, 103 | lat: json.latitude, 104 | city: json.city, 105 | zipCode: json.zip_code, 106 | regionCode: json.region_code, 107 | country: json.country_code, 108 | }; 109 | this.activeClients[client.id].location = location; 110 | 111 | /** 112 | * Creates a base initiator if there is no avaliable initiator 113 | * If initiators are available, create receiver peer 114 | */ 115 | if (this.serverStats.numInitiators < this.peerConfig.threshold) { 116 | createBaseInitiator(client, this.peerConfig); 117 | } else { 118 | createReceiver(client, this.activeClients, this.peerConfig, this.serverStats); 119 | } 120 | }) 121 | .catch((err) => { 122 | /** if API fetch fails, turn off geolocate and create a new initiator */ 123 | console.log(err); 124 | createBaseInitiator(client, this.peerConfig); 125 | }); 126 | } else { 127 | /** If geolocate is off */ 128 | if (this.serverStats.numInitiators < this.peerConfig.threshold || !this.peerConfig.peerImages) { 129 | createBaseInitiator(client, this.peerConfig); 130 | } 131 | else if (this.serverStats.numInitiators >= this.peerConfig.threshold) { 132 | createReceiver(client, this.activeClients, this.peerConfig, this.serverStats); 133 | } 134 | } 135 | 136 | /** 137 | * Initiator sent offer object to server. 138 | * Store offer object to the client's respective object inside this.activeClients. 139 | * Set this client to an initiator and update this.numInitiators count. 140 | */ 141 | client.on('offer_to_server', (message, imageHeights, hasHeights) => { 142 | this.serverStats.numInitiators += 1; 143 | this.activeClients[client.id].initiator = true; 144 | this.activeClients[client.id].offer = message.offer; 145 | if (imageHeights && !this.serverStats.hasHeights) { 146 | this.serverStats.imageHeights = imageHeights; 147 | this.serverStats.hasHeights = hasHeights; 148 | } 149 | console.log(`numClients, numInitiators: ${this.serverStats.numClients}, ${this.serverStats.numInitiators}`); 150 | }); 151 | 152 | /** 153 | * Receiver sent answer object to server. 154 | * Send this answer object to the specific initiator that 155 | * provided the offer object to the receiver. 156 | */ 157 | client.on('answer_to_server', (message, imageSliceIndex) => { 158 | client.to(message.peerId).emit('answer_to_initiator', message.answer, this.activeClients[client.id].location, imageSliceIndex); 159 | }); 160 | 161 | /** 162 | * If the diconnected client was an initiator, 163 | * update accordingly with this.numClients as well 164 | */ 165 | client.on('disconnect', () => { 166 | console.log(`disconnecting ${client.id}`); 167 | if (this.activeClients[client.id].initiator) { 168 | this.serverStats.numInitiators -= 1; 169 | } 170 | delete this.activeClients[client.id]; 171 | this.serverStats.numClients -= 1; 172 | console.log(`numClients, numInitiators: ${this.serverStats.numClients}, ${this.serverStats.numInitiators}`); 173 | }); 174 | client.on('error', err => console.log(err)); 175 | }); 176 | } 177 | 178 | /** Creates initiators after ip geolocation api call 179 | * @param {object} client - Socket client 180 | * @param {object} peerConfig - Config preset by user 181 | */ 182 | function createBaseInitiator(client, peerConfig) { 183 | client.emit('create_base_initiator', peerConfig.assetTypes, peerConfig.foldLoading, this.serverStats.hasHeights); 184 | } 185 | 186 | /** Creates receiver peers after ip geolocation api call 187 | * @param {object} client - Socket client 188 | * @param {object} activeClients - list of active clients to connect to 189 | * @param {object} peerConfig - Config preset by user 190 | * @param {object} serverStats - Information object held by server to update 191 | */ 192 | function createReceiver(client, activeClients, peerConfig, serverStats) { 193 | this.serverStats = serverStats; 194 | this.activeClients = activeClients; 195 | /** checks if geolocate peerConfig is on */ 196 | if (peerConfig.geolocate) { 197 | /** current client's location */ 198 | const clientLocation = this.activeClients[client.id].location; 199 | /** placeholder for the closest peer */ 200 | const closestPeer = { 201 | id: '', 202 | distance: Infinity, 203 | }; 204 | 205 | let tempLocation = null; 206 | let tempDistance = 0; 207 | /** 208 | * Iterate through this.activeClients to find closest initiator avaliable 209 | * make that initiator unavaliable (initiator key set to false). 210 | */ 211 | Object.values(this.activeClients).forEach((clientObj) => { 212 | if (clientObj.initiator) { 213 | tempLocation = this.activeClients[clientObj.id].location; 214 | tempDistance = distance( 215 | clientLocation.lat, 216 | clientLocation.lgn, 217 | tempLocation.lat, 218 | tempLocation.lgn, 219 | ); 220 | if (tempDistance <= closestPeer.distance) { 221 | closestPeer.id = clientObj.id; 222 | closestPeer.distance = tempDistance; 223 | } 224 | } 225 | }); 226 | const selectedInitiator = this.activeClients[closestPeer.id]; 227 | const initiatorData = { 228 | offer: selectedInitiator.offer, 229 | peerId: closestPeer.id, 230 | location: selectedInitiator.location, 231 | }; 232 | this.activeClients[closestPeer.id].initiator = false; 233 | /** Updates this.numInitiators and emit to receiver and send initiator data */ 234 | this.serverStats.numInitiators -= 1; 235 | client.emit('create_receiver_peer', initiatorData, peerConfig.assetTypes, peerConfig.foldLoading, this.serverStats.imageHeights); 236 | } else { 237 | /** loops through activeClients and randomly finds avaliable initiator */ 238 | const initiatorsArr = []; 239 | Object.values(this.activeClients).forEach((clientObj) => { 240 | if (clientObj.initiator) initiatorsArr.push(clientObj.id); 241 | }); 242 | const selectedInitiatorId = initiatorsArr[Math.floor(Math.random() * initiatorsArr.length)]; 243 | const initiatorData = { 244 | offer: this.activeClients[selectedInitiatorId].offer, 245 | peerId: selectedInitiatorId, 246 | }; 247 | this.activeClients[selectedInitiatorId].initiator = false; 248 | /** Updates this.numInitiators and emit to receiver and send initiator data */ 249 | this.serverStats.numInitiators -= 1; 250 | client.emit('create_receiver_peer', initiatorData, peerConfig.assetTypes, peerConfig.foldLoading); 251 | } 252 | } 253 | 254 | /** 255 | * Function to calculate distance using two sets of coordindates 256 | * Source: https://www.geodatasource.com/developers/javascript 257 | */ 258 | function distance(lat1, lon1, lat2, lon2) { 259 | const radlat1 = Math.PI * (lat1 / 180); 260 | const radlat2 = Math.PI * (lat2 / 180); 261 | const theta = lon1 - lon2; 262 | const radtheta = Math.PI * (theta / 180); 263 | let dist = (Math.sin(radlat1) * Math.sin(radlat2)); 264 | dist += (Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta)); 265 | dist = Math.acos(dist); 266 | dist = (dist * 180) / Math.PI; 267 | dist = dist * 60 * 1.1515; 268 | return dist; 269 | } 270 | 271 | function lowerCaseConfig(arr) { 272 | return 273 | } 274 | 275 | /** 276 | * Function that handles torrents and video files 277 | * @constructor 278 | * @param {object} app - app from express() 279 | * @param {object} peerConfig - config preset by user 280 | */ 281 | function VideoConnect (app, peerConfig) { 282 | const createTorrent = require('create-torrent'); 283 | const fs = require('fs'); 284 | const path = require('path'); 285 | 286 | const videoRoute = peerConfig.videoRoute; 287 | const torrentRoute = peerConfig.torrentRoute; 288 | const domainName = peerConfig.domainName; 289 | 290 | fs.readdir(appDir + videoRoute.slice(1), (err, files) => { 291 | if (err) { 292 | console.log(err); 293 | } 294 | 295 | /** Creates routes for each mp4 file to serve as webseeds */ 296 | files.forEach(file => { 297 | app.get(`/video/${file}`, (req, res) => { 298 | res.sendFile(appDir + route.slice(1) + file); 299 | }); 300 | }); 301 | }); 302 | 303 | 304 | /** If torrent folder already exists, just create routes */ 305 | if (fs.existsSync(`${torrentRoute}/torrent`)) { 306 | fs.readdir(appDir + videoRoute.slice(1), (err, files) => { 307 | if (err) { 308 | console.log(err); 309 | } 310 | 311 | /** Loops through video files and create torrent routes that send torrent files */ 312 | files.forEach(file => { 313 | app.get(`/torrent/${file.slice(0, -4)}.torrent`, (req, res) => { 314 | res.sendFile(appDir + `${torrentRoute.slice(1)}/torrent/` + `${file.slice(0, -4)}.torrent`); 315 | }); 316 | }); 317 | }); 318 | return 319 | } 320 | 321 | /** Makes torrent directory */ 322 | fs.mkdir(`${torrentRoute}/torrent`); 323 | 324 | fs.readdir(appDir + videoRoute.slice(1), (err, files) => { 325 | if (err) { 326 | console.log(err); 327 | } 328 | 329 | files.forEach(file => { 330 | /** THIS IS FOR ACTUAL */ 331 | /** Creates torrents with the mp4 links as webseed */ 332 | // createTorrent(appDir + videoRoute.slice(1) + '/' + file, { urlList: [`${domainName}/video/${file}`] }, (err, torrent) => { 333 | /** THIS IS FOR TEST */ 334 | createTorrent(appDir + videoRoute.slice(1) + '/' + file, { urlList: [`${domainName}/${file}`] }, (err, torrent) => { 335 | fs.writeFile(appDir + `${torrentRoute.slice(1)}/torrent/${file.slice(0 , -4)}.torrent`, torrent, (err) => { 336 | if (err) { 337 | console.log(err) 338 | } 339 | }); 340 | }); 341 | 342 | /** Creates routes to serve torrent files according to name */ 343 | app.get(`/torrent/${file.slice(0, -4)}.torrent`, (req, res) => { 344 | res.sendFile(appDir + `${torrentRoute.slice(1)}/torrent/` + `${file.slice(0, -4)}.torrent`); 345 | }); 346 | }); 347 | }); 348 | } 349 | 350 | module.exports = PeerConnectServer; 351 | -------------------------------------------------------------------------------- /client/peer-connect-client.js: -------------------------------------------------------------------------------- 1 | const Peer = require('simple-peer'); 2 | const parseTorrent = require('parse-torrent'); 3 | const http = require('stream-http'); 4 | const WebTorrent = require('webtorrent'); 5 | 6 | /** 7 | * adds methods to peers 8 | * @param {object} peer - peer object 9 | */ 10 | function peerMethods(peer) { 11 | peer.on("error", err => { 12 | console.log(err); 13 | }); 14 | 15 | /* Signal is automatically called when a new peer is created with {initiator:true} parameter. This generates the offer object to be sent to the peer. 16 | Upon receiving the offer object by the receiver, invoke p.signal with the offer object as its parameter. This will generate the answer object. Do the same with the host with the answer object. */ 17 | peer.on("signal", (data) => { 18 | handleOnSignal(data, peerId); 19 | }); 20 | 21 | // listener for when P2P is established. Ice candidates sent first, then media data itself. 22 | peer.on('connect', () => { 23 | handleOnConnect(); 24 | }) 25 | 26 | // listener for when data is being received 27 | peer.on('data', function (data) { 28 | handleOnData(data); 29 | }) 30 | 31 | peer.on('close', function () { 32 | assetsDownloaded ? createInitiator() : createInitiator('base'); 33 | }) 34 | }; 35 | 36 | // peer configuration object from server 37 | const configuration = {}; 38 | 39 | // placeholder for webrtc peer and socket 40 | // socket placeholder is for when page is opened on mobile. 41 | // if no placeholder, browser logs reference error to socket. 42 | let socket = { on: () => { } }; 43 | let p = null; 44 | 45 | // track if assets have been downloaded, determines if peer can be an initiator 46 | let assetsDownloaded = false; 47 | 48 | // peerID is the the socket.id of the initiator. 49 | let peerId = ''; 50 | 51 | // candidates is an array of the ice candidates to send once p2p is established 52 | let candidates = []; 53 | let connectionFlag = false; 54 | 55 | // global variables for data parsing/transfer and fold image loading 56 | let imageData; 57 | let counter = 0; 58 | let extCounter = 0; 59 | let otherCounter = 0; 60 | 61 | // get img tag nodes 62 | let imageArray = Object.values(document.getElementsByTagName('img')); 63 | imageArray = imageArray.filter(node => node.hasAttribute('data-src')); 64 | let imageHeights; 65 | let imageSliceIndex; 66 | const inViewportArray = []; 67 | 68 | // assign ids to image 69 | imageArray.forEach((image, index) => image.setAttribute('id', index)); 70 | 71 | //get video tag nodes 72 | let videoArray = Object.values(document.getElementsByTagName('video')); 73 | videoArray = videoArray.filter(node => node.hasAttribute('data-src')); 74 | 75 | // checks if broswer is opened from mobile 76 | const isMobile = checkForMobile(); 77 | const browserSupport = !!RTCPeerConnection; 78 | 79 | // if webrtc not supported, load from server 80 | if (!browserSupport) { 81 | loadAssetsFromServer(); 82 | } else { 83 | socket = io.connect(); 84 | } 85 | 86 | socket.on('create_base_initiator', createBaseInitiator); 87 | socket.on('create_receiver_peer', createReceiverPeer); 88 | socket.on('answer_to_initiator', answerToInitiator); 89 | socket.on('torrent', getTorrentFiles); 90 | socket.on('load_server_video', loadVideosFromServer); 91 | 92 | /** 93 | * 94 | * @param {array} assetTypes - image asset types for server/peer loading 95 | * @param {boolean} foldLoading - determines doing fold loading or not 96 | * @param {boolean} hasHeights - determines if signalling server has heights or not 97 | */ 98 | function createBaseInitiator(assetTypes, foldLoading, hasHeights) { 99 | // save peer configuration object to front end for host 100 | configuration.assetTypes = assetTypes; 101 | configuration.foldLoading = foldLoading; 102 | 103 | // download assets from server, create initiator peer 104 | // tell server assets were downloaded and send answer object to server 105 | // (this happens when new peer is created with initiator key true) 106 | if (assetTypes.length === 0) { 107 | loadAssetsFromServer(); 108 | return 109 | } 110 | createInitiator(true, hasHeights); 111 | } 112 | 113 | /** 114 | * creates receiver peer 115 | * @param {object} initiatorData - information regarding initiator peer 116 | * @param {array} assetTypes - image asset types for server/peer loading 117 | * @param {boolean} foldLoading - determines doing fold loading or not 118 | * @param {array} imageHeights - image heights necessary for fold loading 119 | */ 120 | function createReceiverPeer(initiatorData, assetTypes, foldLoading, imageHeights) { 121 | // checks if none of the asset types are to be sent through P2P 122 | // if none, load straight from server 123 | let P2PFlag = false; 124 | imageArray.forEach((image) => { 125 | for (let i = 0; i < assetTypes.length; i += 1) { 126 | if ((image.dataset.src).slice(-5).includes(assetTypes[i])) P2PFlag = true; 127 | } 128 | }); 129 | if (!P2PFlag) { 130 | loadAssetsFromServer(); 131 | return; 132 | } 133 | 134 | // save peer configuration object to front end for peer 135 | configuration.assetTypes = assetTypes; 136 | configuration.foldLoading = foldLoading; 137 | p = new Peer({ 138 | initiator: false, 139 | trickle: false, 140 | reconnectTimer: 100, 141 | }); 142 | peerMethods(p); 143 | 144 | //setimageheights and decide which indeces of image you need to send 145 | setImageHeights(imageArray, imageHeights); 146 | 147 | for (let i = 0; i < imageArray.length; i += 1) { 148 | inViewportArray.push(isElementInViewport(imageArray[i])) 149 | } 150 | 151 | //if foldLoading is off || if foldLoading is on and image is not in view 152 | //send indeces of imageArray to request from initiator peer 153 | for (let i = 0; i < imageArray.length; i += 1) { 154 | if ((!inViewportArray[i] && configuration.foldLoading) || !configuration.foldLoading) { 155 | imageSliceIndex = i; 156 | break; 157 | } 158 | } 159 | p.signal(initiatorData.offer); 160 | 161 | // peerId is the socket id of the avaliable initiator that this peer will pair with 162 | peerId = initiatorData.peerId; 163 | } 164 | 165 | /** 166 | * receives answer from peer before connection 167 | * @param {object} message - message object that tells what kind of signal 168 | * @param {object} peerLocation - location information 169 | */ 170 | function answerToInitiator(message, peerLocation) { 171 | //initiator now knows where to slice array before sending to peer 172 | imageSliceIndex = imageSliceIndex; 173 | 174 | // this final signal where initiator receives the answer does not call 175 | // handleOnSignal/.on('signal'), it goes handleOnConnect. 176 | p.signal(message); 177 | setTimeout(checkForConnection, 3000); 178 | } 179 | 180 | /** 181 | * gets torrent file title 182 | * @param {string} torrent - torrent file title 183 | */ 184 | function getTorrentFiles(torrent) { 185 | http.get(`/torrent/${torrent}`, function (res) { 186 | const data = []; 187 | 188 | res.on('data', function (chunk) { 189 | data.push(chunk); 190 | }); 191 | 192 | res.on('end', function () { 193 | let newData = Buffer.concat(data); // Make one large Buffer of it 194 | let torrentParsed = parseTorrent(newData); // Parse the Buffer 195 | const client = new WebTorrent(); 196 | client.add(torrentParsed, onTorrent); 197 | }); 198 | 199 | //render video files to where it was specified on data-src 200 | function onTorrent(torrent) { 201 | torrent.files.forEach(function (file) { 202 | file.renderTo(document.querySelector(`[data-src*='${file.name}']`)); 203 | }); 204 | } 205 | }); 206 | } 207 | 208 | /** 209 | * loops through video array to load from server 210 | */ 211 | function loadVideosFromServer() { 212 | videoArray.forEach(element => { 213 | let source = element.dataset.src; 214 | setServerAsset(source); 215 | }); 216 | } 217 | 218 | /** 219 | * handles signals from peers 220 | * @param {object} data - data object sent from peer 221 | */ 222 | function handleOnSignal(data) { 223 | // send offer object to server for server to store 224 | if (data.type === 'offer') { 225 | socket.emit('offer_to_server', { offer: data }, imageHeights); 226 | } 227 | 228 | // send answer object to server for server to send to avaliable initiator 229 | if (data.type === 'answer') { 230 | socket.emit('answer_to_server', { answer: data, peerId }, imageSliceIndex); 231 | } 232 | 233 | // After the offer/answer object is generated, ice candidates are generated as 234 | // well. These are stored to be sent after the P2P connection is established. 235 | if (data.candidate) { 236 | candidates.push(data); 237 | } 238 | } 239 | 240 | /** 241 | * handles when peers are first connected through webrtc 242 | */ 243 | function handleOnConnect() { 244 | connectionFlag = true; 245 | 246 | // send ice candidates if exist 247 | if (candidates.length) { 248 | p.send(JSON.stringify(candidates)); 249 | candidates = []; 250 | } 251 | 252 | // send assets if initiator (uncomment this if trickle off for receiver) 253 | if (assetsDownloaded) { 254 | sendAssetsToPeer(p, imageSliceIndex); 255 | } 256 | } 257 | 258 | /** 259 | * checks for connection between peers 260 | */ 261 | function checkForConnection() { 262 | if (!connectionFlag) { 263 | p.disconnect(); 264 | } 265 | connectionFlag = false; 266 | } 267 | 268 | /** 269 | * handles when data is received 270 | * @param {object} data - data object that is sent from peer 271 | */ 272 | function handleOnData(data) { 273 | const dataString = data.toString(); 274 | if (dataString.slice(0, 1) === '[') { 275 | const receivedCandidates = JSON.parse(data); 276 | receivedCandidates.forEach((ele) => { 277 | p.signal(ele); 278 | }); 279 | // send assets if initiator 280 | // uncomment this if receiver trickle on 281 | // if (assetsDownloaded) { 282 | // sendAssetsToPeer(p) 283 | // } 284 | return; 285 | } 286 | 287 | loopImage(); 288 | 289 | if (dataString.slice(0, 16) == "finished-sending") { 290 | let imageIndex = data.slice(16); 291 | setImage(imageData, imageArray, imageIndex); 292 | imageData = ''; 293 | if (counter + extCounter === imageArray.length) { 294 | assetsDownloaded = true; 295 | p.destroy(); 296 | checkForImageError(); 297 | } 298 | } else { 299 | imageData += dataString; 300 | } 301 | } 302 | 303 | /** 304 | * loops through images to see if image should be loaded from server 305 | */ 306 | function loopImage() { 307 | function returnFunc() { 308 | if (otherCounter >= 1) return; 309 | for (let i = 0; i < imageArray.length; i += 1) { 310 | const imageSource = imageArray[i].dataset.src; 311 | const extension = getImageType(imageArray[i]); 312 | // console.log(`${inViewportArray[i]} is: from ${i}`); 313 | if (!configuration.assetTypes.includes(extension)) { 314 | extCounter += 1; 315 | setServerAsset(imageSource); 316 | } 317 | if (configuration.foldLoading && inViewportArray[i]) { 318 | setServerAsset(imageSource); 319 | } 320 | } 321 | otherCounter += 1; 322 | } 323 | return returnFunc(); 324 | } 325 | 326 | /** 327 | * sets image onto DOM 328 | * @param {string} imageData - image data string 329 | * @param {array} imageArray - array of DOM image nodes 330 | * @param {integer} index - index of the image array 331 | */ 332 | function setImage(imageData, imageArray, index) { 333 | counter += 1; 334 | if ((!inViewportArray[index] && configuration.foldLoading) || !configuration.foldLoading) { 335 | if (imageData.slice(0, 9) === 'undefined') imageArray[index].src = imageData.slice(9); 336 | else imageArray[index].src = imageData; 337 | } 338 | } 339 | 340 | /** 341 | * preset images with heights for fold loading 342 | * @param {array} imageArray - array of DOM image nodes 343 | * @param {array} imageHeights - array of heights for images 344 | */ 345 | function setImageHeights(imageArray, imageHeights) { 346 | imageHeights.forEach((height, idx) => { 347 | imageArray[idx].style.height = `${height}px`; 348 | }); 349 | getBackgroundImages(); 350 | } 351 | 352 | /** 353 | * creates an initiator with checks to see if it has heights and is a base 354 | * @param {boolean} base - determines whether initiator should download from server or not 355 | * @param {boolean} hasHeights - determines whether initiator has necessary heights from server 356 | */ 357 | function createInitiator(base, hasHeights) { 358 | if (base) { 359 | loadAssetsFromServer(); 360 | assetsDownloaded = true; 361 | if (!hasHeights) imageHeights = setImageHeightsToSend(imageArray); 362 | } 363 | p = new Peer({ 364 | initiator: true, 365 | trickle: false, 366 | reconnectTimer: 100, 367 | }); 368 | peerMethods(p); 369 | } 370 | 371 | /** 372 | * sends image assets to peer 373 | * @param {object} peer - Webrtc peer connection object 374 | * @param {integer} sliceIndex - index to slice image array 375 | */ 376 | function sendAssetsToPeer(peer, sliceIndex) { 377 | //slice Array and only send requested data 378 | imageArray = imageArray.slice(sliceIndex); 379 | 380 | //send only if requested by foldLoading 381 | for (let i = 0; i < imageArray.length; i += 1) { 382 | const imageType = getImageType(imageArray[i]); 383 | if (configuration.assetTypes.includes(imageType)) { 384 | sendImage(imageArray[i], peer, i); 385 | } 386 | } 387 | } 388 | 389 | /** 390 | * returns an array of image heights 391 | * @param {array} imageArray - array of DOM image nodes 392 | */ 393 | function setImageHeightsToSend(imageArray) { 394 | return imageArray.map(imageNode => imageNode.height); 395 | } 396 | 397 | /** 398 | * get the type of image element (e.g. jpg) 399 | * @param {object} image - DOM image nodes 400 | */ 401 | function getImageType(image) { 402 | const imageSrc = image.dataset.src; 403 | const regex = /(?:\.([^.]+))?$/; 404 | return regex.exec(imageSrc)[1]; 405 | } 406 | 407 | /** 408 | * sends image data to specified peer 409 | * @param {object} image - DOM image element 410 | * @param {object} peer - peer to send information to 411 | * @param {integer} imageIndex - the index of the image in the image array 412 | */ 413 | function sendImage(image, peer, imageIndex) { 414 | const data = getImageData(image); 415 | const CHUNK_SIZE = 64000; 416 | const n = data.length / CHUNK_SIZE; 417 | let start; 418 | let end; 419 | for (let f = 0; f < n; f += 1) { 420 | start = f * CHUNK_SIZE; 421 | end = (f + 1) * CHUNK_SIZE; 422 | peer.send(data.slice(start, end)); 423 | } 424 | peer.send(`finished-sending${imageIndex}`); 425 | } 426 | 427 | /** 428 | * loads all image assets from the server 429 | */ 430 | function loadAssetsFromServer() { 431 | for (let i = 0; i < imageArray.length; i += 1) { 432 | const imageSrc = imageArray[i].dataset.src; 433 | setServerAsset(imageSrc); 434 | } 435 | } 436 | 437 | /** 438 | * parses stylesheets for any images defined as background images 439 | * and passes style information to getImageData to generate data strings 440 | */ 441 | function getBackgroundImages() { 442 | const sheets = document.styleSheets; 443 | for (const i in sheets) { 444 | const rules = sheets[i].rules || sheets[i].cssRules; 445 | for (const r in rules) { 446 | if (rules[r].selectorText) { 447 | if (rules[r].cssText.includes('background:') || rules[r].cssText.includes('background-image:')) { 448 | const styleString = rules[r].cssText; 449 | const selector = styleString.substring(0, styleString.indexOf(' ')); 450 | const propertyRegex = /background\s*(.*?)\s*;/g; 451 | const bgProperty = propertyRegex.exec(styleString)[0]; 452 | const imgSrc = bgProperty.substring(bgProperty.indexOf('"') + 1, bgProperty.lastIndexOf('"')); 453 | if (selector !== 'body') getImageData(imgSrc, selector, bgProperty); 454 | } 455 | } 456 | } 457 | } 458 | } 459 | 460 | /** 461 | * creates new background image style with data string sent from peer 462 | * and adds it to the appropriate element 463 | * @param {string} selector - selector to receive data background image 464 | * @param {string} bgProperty - original background style defined in stylesheet 465 | * @param {string} dataUrl - dataUrl generated by getImageData for background image 466 | */ 467 | function setBackgroundImage(selector, bgProperty, dataUrl) { 468 | const newProperty = bgProperty.substring(0, bgProperty.indexOf('"') + 1) + dataUrl + bgProperty.substring(bgProperty.lastIndexOf('"'), bgProperty.indexOf(';') + 1); 469 | document.querySelector(selector).style = newProperty; 470 | } 471 | 472 | /** 473 | * generates data url for images to be sent to peers 474 | * @param {object || string} image - dom element || background asset 475 | * @param {string} seleector - selector to receive data background image 476 | * @param {string} bgProperty - original background style defined in stylesheet 477 | */ 478 | function getImageData(image, selector, bgProperty) { 479 | let canvas = document.createElement('canvas'); 480 | let context = canvas.getContext('2d'); 481 | let img = bgProperty ? new Image() : image; 482 | let type = bgProperty ? '' : getImageType(image); 483 | if (bgProperty) img.src = image; 484 | context.canvas.width = img.width; 485 | context.canvas.height = img.height; 486 | context.drawImage(img, 0, 0, img.width, img.height); 487 | if (bgProperty) setBackgroundImage(selector, bgProperty, canvas.toDataURL()); 488 | return canvas.toDataURL(`image/${type}`); 489 | } 490 | 491 | /** 492 | * check to see if an image element is in view 493 | * @param {object} element - an image node 494 | */ 495 | function isElementInViewport(el) { 496 | const rect = el.getBoundingClientRect(); 497 | return ( 498 | rect.top >= 0 && 499 | rect.left >= 0 && 500 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 501 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 502 | ); 503 | } 504 | 505 | /** 506 | * checks to see if the end user is on mobile 507 | */ 508 | function checkForMobile() { 509 | testExp = new RegExp('Android|webOS|iPhone|iPad|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile', 'i'); 510 | return !!testExp.test(navigator.userAgent); 511 | } 512 | 513 | /** 514 | * goes through images and adds an onerror function to serve assets from server 515 | * @param {array} imageArray - an array of all the image nodes found on a document 516 | */ 517 | function checkForImageError() { 518 | for (let i = 0; i < imageArray.length; i++) { 519 | let source = imageArray[i].dataset.src; 520 | imageArray[i].onerror = function () { 521 | setServerAsset(source); 522 | } 523 | } 524 | } 525 | 526 | /** 527 | * finds the element with the data-src and sets that as the src 528 | * @param {string} imageSource - the source link for image assets stored in the server 529 | */ 530 | function setServerAsset(imageSource) { 531 | document.querySelector(`[data-src='${imageSource}']`).setAttribute('src', `${imageSource}`); 532 | } 533 | --------------------------------------------------------------------------------