11 |
12 | [](https://github.com/peerconnect/peer-connect/blob/master/LICENSE)
13 | [](https://www.npmjs.com/package/peer-connect-client)
14 | [](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 |
--------------------------------------------------------------------------------