From 9fcdf5411391711c8b6a13887cac7b528d63cb02 Mon Sep 17 00:00:00 2001 From: ale Date: Wed, 2 Dec 2020 21:13:47 +0000 Subject: [PATCH] initial commit --- README.md | 13 ++ docker-compose.yml | 23 ++ p2p-media/Dockerfile | 8 + p2p-media/config.json | 21 ++ p2p-media/entrypoint.sh | 5 + p2p-media/index.js | 26 +++ p2p-media/package.json | 28 +++ p2p-media/public/css/main.css | 195 ++++++++++++++++ p2p-media/public/index.html | 62 +++++ p2p-media/public/js/main.js | 428 ++++++++++++++++++++++++++++++++++ 10 files changed, 809 insertions(+) create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 p2p-media/Dockerfile create mode 100644 p2p-media/config.json create mode 100644 p2p-media/entrypoint.sh create mode 100644 p2p-media/index.js create mode 100644 p2p-media/package.json create mode 100644 p2p-media/public/css/main.css create mode 100644 p2p-media/public/index.html create mode 100644 p2p-media/public/js/main.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..a428833 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# p2p-media + +### p2p multimedia software based on [p2p-media-loader](https://github.com/Novage/p2p-media-loader) + +## Run + +``` +docker-compose up -d +``` + +## License + +### MIT diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7b1f52d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '2' + +services: + p2p-media: + build: ./p2p-media + container_name: p2p-media + hostname: p2p-media + restart: always + entrypoint: + - /bin/bash + - /home/node/p2p-media/entrypoint.sh + volumes: + - ./p2p-media:/home/node/p2p-media + - ./p2p-media/config.json:/home/node/wt-tracker/config.json:ro + expose: + - 8080 + - 9000 + networks: + p2p-net: + +networks: + p2p-net: + diff --git a/p2p-media/Dockerfile b/p2p-media/Dockerfile new file mode 100644 index 0000000..6572bd8 --- /dev/null +++ b/p2p-media/Dockerfile @@ -0,0 +1,8 @@ +FROM node:10-slim +RUN apt update && apt -y upgrade && apt -y install git python build-essential && apt clean +USER node +WORKDIR /home/node +RUN git clone https://github.com/Novage/wt-tracker +WORKDIR /home/node/wt-tracker +RUN npm i +RUN npm run build diff --git a/p2p-media/config.json b/p2p-media/config.json new file mode 100644 index 0000000..2a71b8e --- /dev/null +++ b/p2p-media/config.json @@ -0,0 +1,21 @@ +{ + "servers": [ + { + "server": { + "port": 9000, + "host": "0.0.0.0" + }, + "websockets": { + "path": "/*", + "maxPayloadLength": 65536, + "idleTimeout": 240, + "compression": 1, + "maxConnections": 0 + } + } + ], + "tracker": { + "maxOffers": 20, + "announceInterval": 120 + } +} \ No newline at end of file diff --git a/p2p-media/entrypoint.sh b/p2p-media/entrypoint.sh new file mode 100644 index 0000000..09bb180 --- /dev/null +++ b/p2p-media/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash +npm start config.json & +cd /home/node/p2p-media +npm i +node index.js \ No newline at end of file diff --git a/p2p-media/index.js b/p2p-media/index.js new file mode 100644 index 0000000..92eed9a --- /dev/null +++ b/p2p-media/index.js @@ -0,0 +1,26 @@ +const http = require('http'), + express = require('express'), + app = express(), + geoip = require('geoip-lite'), + morgan = require('morgan'), + compression = require('compression'), + rfs = require('rotating-file-stream'), + accessLogStream = rfs.createStream('access.log', { + interval: '1d', + path: __dirname + '/logs', + compress: 'gzip' + }), + server = http.createServer(app).listen(8080, () => { + console.log(`Listening on ${server.address().address}:${server.address().port}`) + }) + +app.disable('x-powered-by') + .use(compression({ level: 9 })) + .use(morgan('combined', { stream: accessLogStream })) + .use(express.json()) + .post('/ip', (req, res) => { + if (req.body.ip.match(/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/)) { + res.json(geoip.lookup(req.body.ip)) + } + }) + .use(express.static(__dirname + '/public')) \ No newline at end of file diff --git a/p2p-media/package.json b/p2p-media/package.json new file mode 100644 index 0000000..2c93258 --- /dev/null +++ b/p2p-media/package.json @@ -0,0 +1,28 @@ +{ + "name": "p2p-media", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "install": "browserify -r p2p-graph | terser -m -c > public/js/p2p-graph.js && cp node_modules/clappr/dist/clappr.min.js node_modules/hls.js/dist/hls.min.js node_modules/level-selector/dist/level-selector.min.js node_modules/p2p-media-loader-hlsjs/build/p2p-media-loader-hlsjs.min.js node_modules/p2p-media-loader-core/build/p2p-media-loader-core.min.js public/js/ && cp node_modules/ipfs/dist/index.min.js public/js/ipfs.min.js && cp node_modules/hlsjs-ipfs-loader/dist/index.min.js public/js/hlsjs-ipfs-loader.min.js" + }, + "author": "ale", + "license": "MIT", + "dependencies": { + "browserify": "*", + "clappr": "*", + "compression": "*", + "express": "*", + "geoip-lite": "*", + "hls.js": "*", + "hlsjs-ipfs-loader": "*", + "ipfs": "*", + "level-selector": "github:clappr/clappr-level-selector-plugin", + "morgan": "*", + "p2p-graph": "*", + "p2p-media-loader-core": "*", + "p2p-media-loader-hlsjs": "*", + "rotating-file-stream": "*", + "terser": "*" + } +} diff --git a/p2p-media/public/css/main.css b/p2p-media/public/css/main.css new file mode 100644 index 0000000..e937158 --- /dev/null +++ b/p2p-media/public/css/main.css @@ -0,0 +1,195 @@ +body { + font: normal 400 16px 'Roboto', sans-serif; + color: #333333; + margin-top: 1em; + margin-bottom: 3em; +} + +.main-header .title { + text-align: center; +} + +.container { + padding: 0 15px; + margin: 0 auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +#main-view *, +::after, +::before { + box-sizing: border-box; +} + +#main-view .wrapper { + display: flex; + flex-wrap: wrap; +} + +#main-view .wrapper .column-1, +#main-view .wrapper .column-2 { + width: 100%; +} + +@media (min-width: 768px) { + #main-view .wrapper .column-1 { + flex: 0 0 66.666667%; + max-width: 66.666667%; + padding-right: 30px; + } + + #main-view .wrapper .column-2 { + flex: 0 0 33.333333%; + max-width: 33.333333%; + } +} + +#video { + width: 100%; +} + +.embed-responsive { + position: relative; + display: block; + width: 100%; + padding: 0; + overflow: hidden; +} + +#main-view .form-group { + display: flex; + flex-direction: column; + margin-bottom: 1em; +} + +#main-view .form-group label { + margin-bottom: 0.5em; +} + +#main-view .form-control { + display: block; + width: 100%; + height: calc(2.25em + 2px); + padding: .375em .75em; + font-size: 1em; + line-height: 1.5em; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: .25em; +} + +#main-view .form-button { + text-align: center; + white-space: nowrap; + border: 1px solid transparent; + padding: .375em .75em; + font-size: 1em; + line-height: 1.5em; + border-radius: .25em; + color: #fff; + background-color: #972e2d; + border-color: #972e2d; + margin-bottom: 5px; +} + +#main-view .form-button:hover { + background-color: #c60000; + border-color: #c60000; +} + +#main-view .form-button:focus { + outline: none; +} + +#main-view .embed-responsive::before { + display: block; + content: ""; +} + +#main-view .embed-responsive .embed-responsive-item { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +#main-view .embed-responsive-16by9::before { + padding-top: 56.25%; +} + +#main-view #level { + margin-top: 2px; + width: auto; + float: right; +} + +#graph { + max-width: 100%; + overflow: hidden; + margin: 3em auto; + border: 1px solid #eee; +} + +#chart_container { + position: relative; + margin: 3em auto; + padding-left: 20px; + max-width: 100%; +} + +#y_axis { + position: absolute; + top: 0; + width: 40px; + left: -20px; +} + +#y_axis>svg { + overflow: visible; +} + +#legend { + position: absolute; + top: 20px; + left: 40px; + z-index: 1; +} + +#legend-totals { + position: absolute; + bottom: 20px; + left: 40px; + z-index: 1; +} + +#main-view .hide { + display: none; +} \ No newline at end of file diff --git a/p2p-media/public/index.html b/p2p-media/public/index.html new file mode 100644 index 0000000..6b98fa9 --- /dev/null +++ b/p2p-media/public/index.html @@ -0,0 +1,62 @@ + + + + + + P2P + + + + + + + + + + + + + + +
+
+

P2P - HatThieves

+
+ +
+
+ WebRTC Data Channels API is not supported by your browser. P2P disabled.
+ Read more at Is WebRTC ready yet?. +
+
+
+ Your browser doesn't support hls.js engine. P2P disabled.
+ Read more at Media + Source Extensions. +
+
+
+ Your browser doesn't support Shaka Player engine. P2P disabled.
+ Read more at Media + Source Extensions. +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/p2p-media/public/js/main.js b/p2p-media/public/js/main.js new file mode 100644 index 0000000..4320975 --- /dev/null +++ b/p2p-media/public/js/main.js @@ -0,0 +1,428 @@ +function waitForGlobalObject(objectName, objectNextName) { + return new Promise((resolve) => { + function check() { + if ((window[objectName] !== undefined) + && ((objectNextName === undefined) || window[objectName][objectNextName] !== undefined)) { + resolve(); + } else { + setTimeout(check, 200); + } + } + + check(); + }); +} + +function waitForModule(moduleName) { + return new Promise((resolve) => { + function check() { + try { + resolve(require(moduleName)); + } catch (e) { + setTimeout(check, 200); + } + } + + check(); + }); +} + +function loadScript(src) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.onload = () => { + resolve(); + }; + script.onerror = () => { + console.log("Failed to load script", src); + reject(); + }; + script.src = src; + document.head.appendChild(script); + }); +} + +class App { + async init() { + await waitForGlobalObject("p2pml", "core", "Ipfs", "HlsjsIpfsLoader", "Hls"); + + this.isP2PSupported = p2pml.core.HybridLoader.isSupported(); + if (!this.isP2PSupported) { + document.querySelector("#error-webrtc-data-channels").classList.remove("hide"); + } + + var params = (new URL(document.location)).searchParams; + + this.videoUrl = params.get('url') ? params.get('url') : 'https://hls.hatthieves.es/hls/streaming.m3u8'; + this.videoContainer = document.getElementById("video_container"); + + this.loadSpeedTimespan = 10; // seconds + + const P2PGraphClass = await waitForModule("p2p-graph"); + this.graph = new P2PGraphClass("#graph"); + this.graph.add({ id: "me", name: "TĂș", me: true }); + + await waitForGlobalObject("Rickshaw"); + + this.initChart(); + + this.restartApp(); + } + + async restartApp() { + this.downloadStats = []; + this.downloadTotals = { http: 0, p2p: 0 }; + this.uploadStats = []; + this.uploadTotal = 0; + + while (this.videoContainer.hasChildNodes()) { + this.videoContainer.removeChild(this.videoContainer.lastChild); + } + + const config = { + loader: { + trackerAnnounce: 'wss://p2p.hatthieves.es/ws' + }, + rtcConfig: { + iceServers: [ + { urls: "stuns:hatthieves.es:3479" }, + { urls: "stun:hatthieves.es:3478" } + ] + } + }; + + await loadScript("js/hls.min.js"); + if (!Hls.isSupported()) { + document.querySelector("#error-hls-js").classList.remove("hide"); + } + this.engine = this.isP2PSupported ? new p2pml.hlsjs.Engine(config) : undefined; + + this.initClapprPlayer(); + + if (this.isP2PSupported) { + this.engine.on(p2pml.core.Events.PieceBytesDownloaded, this.onBytesDownloaded.bind(this)); + this.engine.on(p2pml.core.Events.PieceBytesUploaded, this.onBytesUploaded.bind(this)); + } + + this.refreshChart(); + this.refreshGraph(); + } + + async initClapprPlayer() { + const scriptsPromise = (async () => { + await loadScript("js/clappr.min.js"); + await loadScript("js/level-selector.min.js"); + })(); + + var outer = document.createElement("div"); + outer.className = "embed-responsive embed-responsive-16by9"; + var video = document.createElement("div"); + video.id = "video"; + video.className = "embed-responsive-item"; + outer.appendChild(video); + this.videoContainer.appendChild(outer); + + const params = (new URL(document.location)).searchParams + + const setup = { + parentId: "#video", + plugins: [], + source: params.get('key') && params.get('key').length > 0 ? params.get('key') : this.videoUrl, + width: "100%", + height: "100%", + muted: false, + autoPlay: true, + poster: 'https://www.hatthieves.es/wp-content/uploads/2020/04/cropped-blackground3-2.jpg', + playbackNotSupportedMessage: 'Play is not supported', + watermark: 'https://www.hatthieves.es/wp-content/uploads/2019/08/cropped-ht.png', position: 'top-left', + watermarkLink: 'https://www.hatthieves.es', + mediacontrol: { seekbar: "#ffffff", buttons: "#ffffff" }, + playback: { playInline: true, preload: true, maxBufferLength: 30 } + }; + + if (params.get('key') && params.get('key').length > 0) { + const repoPath = 'ipfs-' + Math.random(), + node = await Ipfs.create({ + repo: repoPath + }), + ipfsHash = params.get('hash') ? params.get('hash') : '', + key = params.get('key') ? params.get('key') : '' + setup.playback.hlsjsConfig = { + debug: false, + loader: HlsjsIpfsLoader, + ipfs: node, + ipfsHash: ipfsHash + }; + document.getElementById('chart_container').style.display = 'none' + document.getElementById('graph').parentNode.style.display = 'none' + document.getElementById('video_container').parentNode.style.flex = '0 0 100%' + document.getElementById('video_container').parentNode.style['max-width'] = '100%' + } else { + setup.playback.hlsjsConfig = { +// liveSyncDurationCount: 7, + loader: this.isP2PSupported ? this.engine.createLoaderClass() : Hls.DefaultConfig.loader + }; + } + + await scriptsPromise; + + setup.plugins.push(LevelSelector); + + var player = new Clappr.Player(setup); + + if (this.isP2PSupported) { + p2pml.hlsjs.initClapprPlayer(player); + } + } + + initChart() { + var chartConf = { + element: document.querySelector("#chart"), + renderer: 'multi', + interpolation: "basis", + stack: false, + min: 'auto', + strokeWidth: 1, + series: [ + { name: "Upload P2P", color: "#88eab9", data: [], renderer: 'area' }, + { name: " - P2P", color: "#88b9ea", data: [], renderer: 'area' }, + { name: " - HTTP", color: "#eae288", data: [], renderer: 'area' }, + { name: "Download", color: "#f64", data: [], renderer: 'line' } + ] + }; + + this.chart = new Rickshaw.Graph(chartConf); + + new Rickshaw.Graph.Axis.X({ + graph: this.chart, + tickFormat: () => '' + }); + + new Rickshaw.Graph.Axis.Y({ + graph: this.chart, + orientation: 'left', + element: document.getElementById('y_axis') + }); + + this.legend = new Rickshaw.Graph.Legend({ + graph: this.chart, + element: document.getElementById('legend') + }); + + this.legendTotals = new Rickshaw.Graph.Legend({ + graph: this.chart, + element: document.getElementById("legend-totals") + }); + + this.chart.render(); + setInterval(this.updateChartData.bind(this), 1000); + + var chartResize = () => { + chartConf.width = this.chart.element.clientWidth; + this.chart.configure(chartConf); + this.chart.render(); + }; + + chartResize(); + window.addEventListener("resize", chartResize); + } + + refreshChart() { + if (!this.chart) { + return; + } + + var data0 = this.chart.series[0].data; + var data1 = this.chart.series[1].data; + var data2 = this.chart.series[2].data; + var data3 = this.chart.series[3].data; + var lastX = data0.length > 0 ? data0[data0.length - 1].x : -1; + + var seriesDataMapper = (currentValue, index) => ({ x: index + lastX + 1, y: 0 }); + + data0.length = 0; + data1.length = 0; + data2.length = 0; + data3.length = 0; + + var stubData = Array.apply(null, Array(200)).map(seriesDataMapper); + data0.push.apply(data0, stubData.slice(0)); + data1.push.apply(data1, stubData.slice(0)); + data2.push.apply(data2, stubData.slice(0)); + data3.push.apply(data3, stubData.slice(0)); + + this.chart.update(); + } + + updateChartData() { + var downloadSpeed = this.getDownloadSpeed(); + var http = Number((downloadSpeed.http * 8 / 1000000).toFixed(2)); + var p2p = Number((downloadSpeed.p2p * 8 / 1000000).toFixed(2)); + var total = Number((http + p2p).toFixed(2)); + var upload = Number(this.getUploadSpeed() * 8 / 1000000).toFixed(2); + + var data0 = this.chart.series[0].data; + var data1 = this.chart.series[1].data; + var data2 = this.chart.series[2].data; + var data3 = this.chart.series[3].data; + var x = data0.length > 0 ? data0[data0.length - 1].x + 1 : 0; + + data0.shift(); + data1.shift(); + data2.shift(); + data3.shift(); + data0.push({ x: x, y: -upload }); + data1.push({ x: x, y: total }); + data2.push({ x: x, y: http }); + data3.push({ x: x, y: total }); + this.chart.update(); + + this.formatChartLegendLine(0, total); + this.formatChartLegendLine(1, http); + this.formatChartLegendLine(2, p2p); + this.formatChartLegendLine(3, upload); + + this.updateLegendTotals(); + } + + formatChartLegendLine(index, speed) { + if (this.legend) { + var line = this.legend.lines[index]; + line.element.childNodes[1].textContent = line.series.name + ' - ' + speed + ' Mbit/s'; + } + } + + updateLegendTotals() { + if (!this.legendTotals) { + return; + } + + var httpMb = this.downloadTotals.http / 1048576; + var p2pMb = this.downloadTotals.p2p / 1048576; + var totalMb = httpMb + p2pMb; + var uploadMb = this.uploadTotal / 1048576; + + if (totalMb != 0) { + this.legendTotals.lines[0].element.childNodes[1].textContent + = "Download - " + + Number(totalMb).toFixed(1) + " MiB"; + + this.legendTotals.lines[1].element.childNodes[1].textContent + = " - HTTP - " + + Number(httpMb).toFixed(1) + " MiB - " + + Number((httpMb * 100) / totalMb).toFixed(0) + "%"; + + this.legendTotals.lines[2].element.childNodes[1].textContent + = " - P2P - " + + Number(p2pMb).toFixed(1) + " MiB - " + + Number((p2pMb * 100) / totalMb).toFixed(0) + "%"; + + this.legendTotals.lines[3].element.childNodes[1].textContent + = "Upload P2P - " + + Number(uploadMb).toFixed(1) + " MiB"; + } + } + + getDownloadSpeed() { + var startingPoint = performance.now() - (this.loadSpeedTimespan * 1000); + var httpSize = 0; + var p2pSize = 0; + + var i = this.downloadStats.length; + while (i--) { + var stat = this.downloadStats[i]; + if (stat.timestamp < startingPoint) { + break; + } + + if (stat.method === "p2p") { + p2pSize += stat.size; + } else if (stat.method === "http") { + httpSize += stat.size; + } + } + + this.downloadStats.splice(0, i + 1); + + return { p2p: p2pSize / this.loadSpeedTimespan, http: httpSize / this.loadSpeedTimespan }; + } + + getUploadSpeed() { + var startingPoint = performance.now() - (this.loadSpeedTimespan * 1000); + var size = 0; + + var i = this.uploadStats.length; + while (i--) { + var stat = this.uploadStats[i]; + if (stat.timestamp < startingPoint) { + break; + } + + size += stat.size; + } + + this.uploadStats.splice(0, i + 1); + + return size / this.loadSpeedTimespan; + } + + onBytesDownloaded(method, size) { + this.downloadStats.push({ method: method, size: size, timestamp: performance.now() }); + this.downloadTotals[method] += size; + } + + onBytesUploaded(method, size) { + this.uploadStats.push({ size: size, timestamp: performance.now() }); + this.uploadTotal += size; + } + + refreshGraph() { + if (!this.graph) { + return; + } + + var nodes = this.graph.list(); + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].id !== "me") { + this.graph.disconnect("me", nodes[i].id); + this.graph.remove(nodes[i].id); + } + } + + if (this.isP2PSupported) { + this.engine.on(p2pml.core.Events.PeerConnect, this.onPeerConnect.bind(this)); + this.engine.on(p2pml.core.Events.PeerClose, this.onPeerClose.bind(this)); + } + } + + onPeerConnect(peer) { + if (!this.graph.hasPeer(peer.id)) { + var oReq = new XMLHttpRequest(), + graph = this.graph + oReq.onreadystatechange = function () { + if (oReq.readyState != 4) { return; } + + var location = this.response + graph.add({ id: peer.id, name: location.city + ' - ' + location.region + ' (' + location.country + ')' || 'Unknown' }); + graph.connect("me", peer.id); + } + oReq.open('POST', 'ip', true); + oReq.responseType = 'json'; + oReq.setRequestHeader('Content-Type', 'application/json'); + oReq.send(JSON.stringify({ ip: peer.remoteAddress })); + } + } + + onPeerClose(id) { + if (this.graph.hasPeer(id)) { + this.graph.disconnect("me", id); + this.graph.remove(id); + } + } +} + +window.onload = function () { + window.app = new App(); + window.app.init(); +}