├── Procfile ├── spec ├── factory_tests │ ├── packetHandlersFactory_spec.js │ ├── fileTransferFactory_spec.js │ ├── linkGenerationFactory_spec.js │ ├── webRTCFactory_spec.js │ └── fileUploadFactory_spec.js ├── support │ └── jasmine.json ├── controller_tests │ └── homeController_spec.js └── server_spec.js ├── client ├── app │ ├── components │ │ ├── home │ │ │ ├── homeView.html │ │ │ └── homeController.js │ │ ├── modals │ │ │ ├── guideModalView.html │ │ │ ├── modalController.js │ │ │ ├── badBrowserModalView.html │ │ │ ├── aboutModalView.html │ │ │ └── contactModalView.html │ │ ├── client_utilities │ │ │ ├── client_utilities.js │ │ │ └── factories │ │ │ │ ├── fileTransferFactory.js │ │ │ │ ├── modalFactory.js │ │ │ │ ├── notificationFactory.js │ │ │ │ ├── fileReaderFactory.js │ │ │ │ ├── linkGenerationFactory.js │ │ │ │ ├── lightningButtonFactory.js │ │ │ │ ├── webRTCFactory.js │ │ │ │ ├── fileUploadFactory.js │ │ │ │ └── packetHandlersFactory.js │ │ ├── upload │ │ │ ├── uploadController.js │ │ │ └── uploadView.html │ │ ├── download │ │ │ ├── downloadController.js │ │ │ └── downloadView.html │ │ └── connecting │ │ │ └── connectingController.js │ ├── app_modules.js │ └── app_routes.js ├── assets │ ├── logo.png │ ├── bright.jpg │ ├── bolt_only.png │ ├── logo_plain.png │ ├── cropped-group.jpg │ ├── favicon-bolt.ico │ ├── logo_plain_300.png │ ├── logo_plain_300_white.png │ ├── mkstream_architecture.png │ └── styles.css ├── index.html └── lib │ └── nochunkbufferfixpeer.min.js ├── .gitignore ├── chrome_extension ├── bolt_128.png ├── bolt_only.png ├── chrome_extension.zip ├── background.js └── manifest.json ├── database ├── models │ └── user_schema.js └── config.js ├── server ├── server.js ├── config │ └── middleware.js ├── database_queries │ └── database_queries.js └── routes │ └── webRTC_routes.js ├── bower.json ├── LICENSE ├── package.json ├── karma.conf.js ├── Gruntfile.js └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: nodemon ./server/server.js -------------------------------------------------------------------------------- /spec/factory_tests/packetHandlersFactory_spec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/components/home/homeView.html: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .DS_Store 4 | .env -------------------------------------------------------------------------------- /client/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/client/assets/logo.png -------------------------------------------------------------------------------- /client/assets/bright.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/client/assets/bright.jpg -------------------------------------------------------------------------------- /client/assets/bolt_only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/client/assets/bolt_only.png -------------------------------------------------------------------------------- /chrome_extension/bolt_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/chrome_extension/bolt_128.png -------------------------------------------------------------------------------- /client/assets/logo_plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/client/assets/logo_plain.png -------------------------------------------------------------------------------- /chrome_extension/bolt_only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/chrome_extension/bolt_only.png -------------------------------------------------------------------------------- /client/assets/cropped-group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/client/assets/cropped-group.jpg -------------------------------------------------------------------------------- /client/assets/favicon-bolt.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/client/assets/favicon-bolt.ico -------------------------------------------------------------------------------- /client/assets/logo_plain_300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/client/assets/logo_plain_300.png -------------------------------------------------------------------------------- /client/app/components/modals/guideModalView.html: -------------------------------------------------------------------------------- 1 |
2 |

GUIDE ME ME

3 |
-------------------------------------------------------------------------------- /chrome_extension/chrome_extension.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/chrome_extension/chrome_extension.zip -------------------------------------------------------------------------------- /client/assets/logo_plain_300_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/client/assets/logo_plain_300_white.png -------------------------------------------------------------------------------- /client/assets/mkstream_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAKE-SITY/MKSTream/HEAD/client/assets/mkstream_architecture.png -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "server_spec.js" 5 | ], 6 | "stopSpecOnExpectationFailure": false, 7 | "random": false 8 | } 9 | -------------------------------------------------------------------------------- /client/app/components/modals/modalController.js: -------------------------------------------------------------------------------- 1 | angular.module('modals', [ 2 | 'utils' 3 | ]) 4 | 5 | .controller('modalsController', ['$scope', 'modals', function($scope, modals){ 6 | console.log('hhhhhhh'); 7 | 8 | 9 | }]); -------------------------------------------------------------------------------- /chrome_extension/background.js: -------------------------------------------------------------------------------- 1 | console.log('im the background page'); 2 | 3 | chrome.browserAction.onClicked.addListener(function(activeTab){ 4 | var newURL = "https://www.mkstream.club/#/"; 5 | chrome.windows.create({ url: newURL }); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /database/models/user_schema.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var userSchema = mongoose.Schema({ 4 | linkHash: String, 5 | senderID: String, 6 | receiverIDArray: [String] 7 | }); 8 | 9 | module.exports = mongoose.model('User', userSchema); -------------------------------------------------------------------------------- /client/app/app_modules.js: -------------------------------------------------------------------------------- 1 | angular.module('MKSTream', [ 2 | 'ui.router', 3 | 'ui.bootstrap', 4 | 'ui.bootstrap.modal', 5 | 'ui-notification', 6 | 'clientRoutes', 7 | 'modals', 8 | 'home', 9 | 'connecting', 10 | 'upload', 11 | 'download', 12 | 'utils' 13 | ]); -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var port = process.env.PORT || 3000; 3 | // var peerPort = process.env.PEERPORT || 9000; 4 | var app = express(); 5 | 6 | require('./config/middleware.js')(app, express); 7 | 8 | var server = app.listen(port); 9 | console.log('Now listening on port: ' + port); -------------------------------------------------------------------------------- /client/app/components/client_utilities/client_utilities.js: -------------------------------------------------------------------------------- 1 | angular.module('utils', [ 2 | 'utils.fileReader', 3 | 'utils.fileUpload', 4 | 'utils.linkGeneration', 5 | 'utils.webRTC', 6 | 'utils.packetHandlers', 7 | 'utils.fileTransfer', 8 | 'utils.modals', 9 | 'utils.notifications', 10 | 'utils.lightningButton' 11 | ]); -------------------------------------------------------------------------------- /client/app/components/modals/badBrowserModalView.html: -------------------------------------------------------------------------------- 1 |
2 |

STOP!

3 |

You are using an unsupported browser. You will not be able to use our web application.

4 |

Chrome >37 or Firefox >31 is required. For optimal performance, use the most recent version of Chrome.

5 |
-------------------------------------------------------------------------------- /client/app/components/client_utilities/factories/fileTransferFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('utils.fileTransfer', []) 2 | 3 | .factory('fileTransfer', function() { 4 | 5 | var fileTransfer = {}; 6 | 7 | fileTransfer.incomingFileTransfers = {}; 8 | fileTransfer.outgoingFileTransfers = {}; 9 | fileTransfer.finishedTransfers = []; 10 | fileTransfer.offers = []; 11 | fileTransfer.downloadQueue = []; 12 | return fileTransfer; 13 | 14 | }); -------------------------------------------------------------------------------- /chrome_extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "MKSTream", 4 | "description": "MKSTream's chrome_extension", 5 | "version": "1.0", 6 | "homepage_url": "https://mkstream.club", 7 | "browser_action": { 8 | "default_icon": "./bolt_only.png", 9 | "default_title": "transfer files on mkstream.club" 10 | }, 11 | "background": { 12 | "scripts": ["./background.js"] 13 | }, 14 | "permissions": [ 15 | "windows", 16 | "tabs" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /spec/factory_tests/fileTransferFactory_spec.js: -------------------------------------------------------------------------------- 1 | describe('fileTransfer Factory', function() { 2 | var factory; 3 | beforeEach(function() { 4 | module('utils'); //angular.module name 5 | 6 | inject(function($injector) { 7 | factory = $injector.get('fileTransfer'); //.factory name 8 | }); 9 | }); 10 | 11 | describe('fileTransfer', function() { 12 | it("Should return an object with 5 keys", function() { 13 | var myObj = Object.keys(factory).length; 14 | expect(myObj).toEqual(5); 15 | }); 16 | }); 17 | }); -------------------------------------------------------------------------------- /server/config/middleware.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var morgan = require('morgan'); 4 | 5 | module.exports = function(app, express) { 6 | app.use(morgan('dev')); 7 | app.use(bodyParser.json()); 8 | app.use(express.static(__dirname + './../../client')); 9 | app.use('/bower_components', express.static(__dirname + './../../bower_components')); 10 | 11 | //webRTC 12 | var webRTCRouter = express.Router(); 13 | require('../routes/webRTC_routes.js')(webRTCRouter); 14 | app.use('/api/webrtc', webRTCRouter); 15 | }; -------------------------------------------------------------------------------- /client/app/components/upload/uploadController.js: -------------------------------------------------------------------------------- 1 | angular.module('upload', [ 2 | 'utils', 3 | 'ngAnimate' 4 | ]) 5 | 6 | .controller('uploadController', [ 7 | '$scope', 8 | 'fileTransfer', 9 | 'fileUpload', 10 | function($scope, fileTransfer, fileUpload) { 11 | console.log('upload controller loaded'); 12 | 13 | $scope.incomingFileTransfers = fileTransfer.incomingFileTransfers; 14 | $scope.outgoingFileTransfers = fileTransfer.outgoingFileTransfers; 15 | $scope.acceptFileOffer = fileUpload.acceptFileOffer; 16 | $scope.rejectFileOffer = fileUpload.rejectFileOffer; 17 | $scope.offers = fileTransfer.offers; 18 | $scope.uploadedFiles = fileTransfer.myItems; 19 | 20 | }]); -------------------------------------------------------------------------------- /database/config.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var uri = (process.env.MONGOLAB_URI || 'mongodb://localhost/MKStream'); 4 | 5 | var options = { 6 | server: { 7 | socketOptions:{ 8 | keepAlive: 1, 9 | connectTimeoutMS: 30000 10 | } 11 | }, 12 | replset: { 13 | socketOptions:{ 14 | keepAlive: 1, 15 | connectTimeoutMS: 30000 16 | } 17 | } 18 | }; 19 | 20 | mongoose.connect(uri, options); 21 | 22 | var db = mongoose.connection; 23 | 24 | db.on("error", console.error.bind(console, 'connection error:')); 25 | 26 | db.once("open", function(callback) { 27 | console.log("We've opened a connection to the database"); 28 | }); 29 | 30 | module.exports = db; -------------------------------------------------------------------------------- /client/app/components/download/downloadController.js: -------------------------------------------------------------------------------- 1 | angular.module('download', [ 2 | 'utils', 3 | 'ngAnimate' 4 | ]) 5 | 6 | .controller('downloadController', [ 7 | '$scope', 8 | 'fileTransfer', 9 | 'fileUpload', 10 | function($scope, fileTransfer, fileUpload) { 11 | console.log('download controller loaded'); 12 | 13 | $scope.incomingFileTransfers = fileTransfer.incomingFileTransfers; 14 | $scope.outgoingFileTransfers = fileTransfer.outgoingFileTransfers; 15 | $scope.acceptFileOffer = fileUpload.acceptFileOffer; 16 | $scope.rejectFileOffer = fileUpload.rejectFileOffer; 17 | $scope.offers = fileTransfer.offers; 18 | console.log('download scope', $scope.offers); 19 | 20 | 21 | 22 | 23 | }]); -------------------------------------------------------------------------------- /spec/factory_tests/linkGenerationFactory_spec.js: -------------------------------------------------------------------------------- 1 | describe('linkGeneration Factory', function() { 2 | var factory; 3 | 4 | beforeEach(function() { 5 | //angular.module name 6 | module('utils.linkGeneration'); 7 | 8 | inject(function($injector) { 9 | factory = $injector.get('linkGeneration'); //.factory name 10 | }); 11 | }); 12 | 13 | describe('linkGeneration', function() { 14 | it("Should return an object with three properties", function() { 15 | var myObj = Object.keys(factory).length; 16 | expect(myObj).toEqual(3); 17 | }); 18 | 19 | it("Should have a method called adjAdjAnimal", function() { 20 | expect(factory.adjAdjAnimal).toBeDefined(); 21 | }); 22 | 23 | it("Should have a method called generateHash", function() { 24 | expect(factory.generateHash).toBeDefined(); 25 | }); 26 | }); 27 | }); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mkstream", 3 | "description": "Group thesis project for MakerSquare", 4 | "main": "./client/index.html", 5 | "authors": [ 6 | "MAKE-SITY" 7 | ], 8 | "license": "MIT", 9 | "homepage": "https://github.com/MAKE-SITY/MKSTream", 10 | "moduleType": [], 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "angular": "~1.4.8", 20 | "angular-ui-router": "ui-router#~0.2.15", 21 | "angular-mocks": "~1.4.9", 22 | "angular-resource": "~1.4.9", 23 | "adjective-adjective-animal": "~1.4.0", 24 | "jquery": "~2.2.0", 25 | "localforage": "~1.3.3", 26 | "angular-animate": "~1.4.9", 27 | "angular-ui-notification": "~0.1.0", 28 | "angular-bootstrap": "~1.1.2", 29 | "bootstrap": "~3.3.6", 30 | "font-awesome": "~4.5.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/app/app_routes.js: -------------------------------------------------------------------------------- 1 | angular.module('clientRoutes', []) 2 | 3 | .config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { 4 | 5 | $urlRouterProvider 6 | .otherwise('/'); 7 | 8 | $stateProvider 9 | .state('home', { 10 | url: '/', 11 | controller: 'homeController', 12 | templateUrl: './app/components/home/homeView.html' 13 | }) 14 | .state('room', { 15 | url: '/room/:roomHash', 16 | views: { 17 | 'connecting':{ 18 | controller: 'connectingController' 19 | }, 20 | 'upload':{ 21 | controller: 'uploadController', 22 | templateUrl: './app/components/upload/uploadView.html' 23 | }, 24 | 'download':{ 25 | controller: 'downloadController', 26 | templateUrl: './app/components/download/downloadView.html' 27 | } 28 | } 29 | }); 30 | }]); 31 | -------------------------------------------------------------------------------- /client/app/components/upload/uploadView.html: -------------------------------------------------------------------------------- 1 |
Outgoing Files 2 |
3 |
4 |
5 |
6 |
7 |
File Name: 8 |
{{item.name}}
9 |
10 |
11 |
12 |
13 | File Size: 14 |
{{item.formattedSize}} 15 |
16 |
17 | File Type: {{item.type}} 18 |
19 |
20 | {{item.status}} 21 |
22 |
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /spec/factory_tests/webRTCFactory_spec.js: -------------------------------------------------------------------------------- 1 | describe('webRTC Factory', function() { 2 | var webRTC; 3 | beforeEach(function() { 4 | module('utils'); //angular.module name 5 | inject(function($injector) { 6 | webRTC = $injector.get('webRTC'); 7 | }); 8 | }); 9 | 10 | describe('webRTC', function() { 11 | describe('createPeer', function() { 12 | it('should return a peer object', function() { 13 | webRTC.heartBeat = function() { 14 | // fake heartbeat 15 | }; 16 | var peer = webRTC.createPeer(); 17 | expect(peer.constructor).toBe(Peer); 18 | }); 19 | }); 20 | 21 | describe('clearQueue', function() { 22 | var conn; 23 | beforeEach(function() { 24 | conn = { 25 | send: function() { 26 | // fake send 27 | } 28 | }; 29 | spyOn(conn, 'send'); 30 | }); 31 | 32 | it('should call conn.send', function() { 33 | webRTC.clearQueue([{}], conn); 34 | expect(conn.send).toHaveBeenCalled(); 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /client/app/components/client_utilities/factories/modalFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('utils.modals', []) 2 | 3 | .factory('modals', ['$uibModal', function($uibModal){ 4 | 5 | var modals = {}; 6 | 7 | modals.openModal = function(template){ 8 | var templateUrl; 9 | if(template === 'about'){ 10 | templateUrl = 'app/components/modals/aboutModalView.html'; 11 | } else if (template === 'guide') { 12 | templateUrl = 'app/components/modals/guideModalView.html'; 13 | } else if (template === 'contact') { 14 | templateUrl = 'app/components/modals/contactModalView.html'; 15 | } 16 | $uibModal.open({ 17 | templateUrl: templateUrl, 18 | controller: 'modalsController', 19 | windowClass: 'informational-modal' 20 | }); 21 | 22 | }; 23 | 24 | modals.badBrowser = function(){ 25 | $uibModal.open({ 26 | templateUrl: 'app/components/modals/badBrowserModalView.html', 27 | controller: 'modalsController', 28 | windowClass: 'informational-modal' 29 | }); 30 | } 31 | 32 | return modals; 33 | 34 | }]); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 MKSTeam 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. 22 | -------------------------------------------------------------------------------- /client/app/components/client_utilities/factories/notificationFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('utils.notifications', [ 2 | 'ui-notification' 3 | ]) 4 | 5 | .factory('notifications', ['Notification', function(Notification){ 6 | 7 | var notifications = {}; 8 | 9 | var notificationPosX = 'center'; 10 | 11 | notifications.successMessage = function(name){ 12 | Notification.success({ 13 | message: name + ' finished downloading', 14 | positionX: notificationPosX 15 | }); 16 | }; 17 | 18 | notifications.connectionLost = function(){ 19 | Notification.error({ 20 | message: 'Connection Lost', 21 | positionX: notificationPosX 22 | }); 23 | }; 24 | 25 | notifications.tabReminder = function(){ 26 | Notification.warning({ 27 | message: 'If you place this tab in the background your connection will slow down. ' + 28 | 'Please move this tab to a new window.', 29 | positionX: notificationPosX, 30 | delay: 20000 31 | }); 32 | }; 33 | 34 | notifications.alreadyUploaded = function(name){ 35 | Notification.error({ 36 | message: 'You already uploaded ' + name, 37 | positionX: notificationPosX 38 | }) 39 | }; 40 | 41 | return notifications; 42 | 43 | }]); -------------------------------------------------------------------------------- /spec/controller_tests/homeController_spec.js: -------------------------------------------------------------------------------- 1 | xdescribe('homeController', function() { 2 | var $scope, 3 | $http, 4 | // $state, 5 | // $stateParams, 6 | $location, 7 | $rootScope, 8 | fileTransfer, 9 | linkGeneration, 10 | webRTC, 11 | packetHandlers; 12 | 13 | beforeEach(function() { 14 | module('home'); 15 | 16 | inject(function($injector) { 17 | $rootScope = $injector.get('$rootScope'); 18 | $scope = $rootScope.$new(); 19 | $http = $injector.get('$http'); 20 | // $state = $injector.get('$state'); 21 | // $stateParams = $injector.get('$stateParams'); 22 | $location = $injector.get('$location'); 23 | fileTransfer = $injector.get('fileTransfer'); 24 | linkGeneration = $injector.get('linkGeneration'); 25 | webRTC = $injector.get('webRTC'); 26 | packetHandlers = $injector.get('packetHandlers'); 27 | 28 | }); 29 | }); 30 | 31 | describe('home', function() { 32 | it("Should contain an object called fileTransfer", function() { 33 | expect(fileTransfer).toEqual({}); 34 | }); 35 | 36 | it("Should contain a key called 'myItems' that is an array", function() { 37 | console.log('THIS IS FILE TRANSFER', fileTransfer); 38 | expect($scope.x).toEqual('x'); 39 | }); 40 | }); 41 | }); -------------------------------------------------------------------------------- /client/app/components/client_utilities/factories/fileReaderFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('utils.fileReader', []) 2 | 3 | .factory('fileReader', ['$q', '$log', function($q, $log) { 4 | var fileReader = {}; 5 | 6 | var onLoad = function(reader, deferred, scope) { 7 | return function() { 8 | scope.$apply(function() { 9 | deferred.resolve(reader.result); 10 | }); 11 | }; 12 | }; 13 | 14 | var onError = function(reader, deferred, scope) { 15 | return function() { 16 | scope.$apply(function() { 17 | deferred.reject(reader.result); 18 | }); 19 | }; 20 | }; 21 | 22 | var onProgress = function(reader, scope) { 23 | return function(event) { 24 | scope.$broadcast('fileProgress', { 25 | total: event.total, 26 | loaded: event.loaded 27 | }); 28 | }; 29 | }; 30 | 31 | var getReader = function(deferred, scope) { 32 | var reader = new FileReader(); 33 | 34 | reader.onload = onLoad(reader, deferred, scope); 35 | reader.onerror = onError(reader, deferred, scope); 36 | reader.onprogress = onProgress(reader, scope); 37 | 38 | return reader; 39 | }; 40 | 41 | fileReader.readAsArrayBuffer = function(file, scope) { 42 | var deferred = $q.defer(); 43 | 44 | var reader = getReader(deferred, scope); 45 | reader.readAsArrayBuffer(file); 46 | 47 | return deferred.promise; 48 | }; 49 | 50 | return fileReader; 51 | 52 | }]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mkstream", 3 | "version": "1.0.0", 4 | "description": "Group thesis project for MakerSquare", 5 | "main": "./server/server.js", 6 | "scripts": { 7 | "start": "nf start", 8 | "test": "karma start && jasmine", 9 | "postinstall": "bower install" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/MAKE-SITY/MKSTream.git" 14 | }, 15 | "author": "MAKE-SITY", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/MAKE-SITY/MKSTream/issues" 19 | }, 20 | "homepage": "https://github.com/MAKE-SITY/MKSTream#readme", 21 | "dependencies": { 22 | "body-parser": "^1.14.2", 23 | "bower": "^1.7.7", 24 | "express": "^4.13.3", 25 | "foreman": "^3.0.1", 26 | "grunt": "^1.4.1", 27 | "grunt-cli": "^1.4.3", 28 | "grunt-contrib-concat": "^0.5.1", 29 | "grunt-contrib-cssmin": "^4.0.0", 30 | "grunt-contrib-jshint": "^3.0.0", 31 | "grunt-contrib-uglify": "^0.11.0", 32 | "grunt-contrib-watch": "^1.1.0", 33 | "grunt-notify": "^0.4.3", 34 | "jasmine": "^2.4.1", 35 | "jasmine-core": "^2.4.1", 36 | "karma": "^6.3.4", 37 | "karma-chrome-launcher": "^0.2.2", 38 | "karma-jasmine": "^0.3.6", 39 | "karma-nyan-reporter": "^0.2.3", 40 | "kerberos": "^1.1.6", 41 | "mongodb": "^4.1.0", 42 | "mongoose": "^5.13.7", 43 | "morgan": "^1.6.1", 44 | "nodemon": "^2.0.12", 45 | "request": "^2.67.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/app/components/modals/aboutModalView.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Until recently, file sharing in browser had to be done by uploading a file to a server, and then sending it from there to the desired destination. 4 |

5 |

6 | MKStream, powered by WebRTC, can be used to create a lightning fast connection for sharing files of any kind with anyone you want. All file sharing is done peer to peer, meaning that the data never touches our servers and remains secure and private. Our database is only used to store the unique links, which are deleted once you finish using them. 7 |

8 | 9 |

10 | MKStream was developed by four full stack software engineers: Tyler Ferrier, Malek Ascha, Kevin Van and Simon Ding. MK Stream uses Angular for the front end, Node/Express and MongoDB for the backend, and Karma and Jasmine for testing. We used PeerJS to harness WebRTC and create our peer-to-peer connection for file sharing. 11 |

12 |
-------------------------------------------------------------------------------- /client/app/components/modals/contactModalView.html: -------------------------------------------------------------------------------- 1 |
2 |

How it works

3 |

4 | For security reasons, if either peer closes or refreshes their tab MKStream will destroy the connection and void the current anonymous link. The link is only good for one connection between two people. If you want to send files to more than one person, you must make a new link for each person. To share more files, just go back to MKStream.club and MKStream will be ready for you any time. 5 |

6 |

Upload files

7 |

8 | Upload your file(s) by clicking the drop-zone and/or dragging and dropping a file into the drop-zone. 9 |

10 |

Share link

11 |

12 | After choosing your file(s), MKStream creates a anonymous and temporary unique url used to transfer files in the browser. Clicking on the glowing lightning bolt below the drop-zone will copy the temporary url to your clipboard. Give this link to another person to establish a connection and begin transfering. 13 |

14 |

Transfer data

15 |

16 | Your url and your file(s) will be available for transfer as long as you keep your MKStream tab open. When both people have the unique url loaded, a file will appear in your receiving section. Clicking accept on a file begins the transfer and readies it for you to download. Any time you click and upload a file or drag and drop it, it will be sent to the other person. The connection is good as long as both people stay on it. If you lose your connection for any reason, go back to MKStream.club to get a new link. 17 |

18 |
-------------------------------------------------------------------------------- /server/database_queries/database_queries.js: -------------------------------------------------------------------------------- 1 | var User = require('./../../database/models/user_schema.js'); 2 | var db = require('./../../database/config.js'); 3 | 4 | var exportObj = {}; 5 | 6 | exportObj.addLink = function(linkHash, senderID) { 7 | return new User({ 8 | linkHash: linkHash, 9 | senderID: senderID, 10 | receiverIDArray: [] 11 | }).save(function(err, addedUser) { 12 | if(err) { 13 | console.log('error trying to save user to DB:', err); 14 | } else { 15 | console.log('addedUser:', addedUser); 16 | } 17 | }); 18 | }; 19 | 20 | exportObj.addReceiverToSender = function(linkHash, receiverID) { 21 | console.log('addRECEIVERToSEnder firing event'); 22 | return User.findOneAndUpdate( 23 | {linkHash: linkHash}, 24 | {$push: {receiverIDArray: receiverID}}, 25 | {safe: true, upsert: true} 26 | ); 27 | }; 28 | 29 | exportObj.deleteLink = function(senderID) { 30 | return User.find({senderID: senderID}).remove(function(err) { 31 | if (err) { 32 | console.log('could not delete', senderID, ':', err); 33 | } else { 34 | console.log('deleted', senderID); 35 | } 36 | }); 37 | }; 38 | 39 | exportObj.getSenderId = function(linkHash) { 40 | return User.findOne({linkHash: linkHash}, function(err, user) { 41 | if (err) { 42 | console.log('could not get user', user, ':', err); 43 | } else { 44 | console.log('retrieved senderID:', user.senderID, 'from linkHash', linkHash); 45 | } 46 | }); 47 | }; 48 | 49 | exportObj.removeReceiverFromSender = function(linkHash, receiverID) { 50 | return User.findOneAndUpdate( 51 | {linkHash: linkHash}, 52 | {$pull: {receiverIDArray: receiverID}} 53 | ); 54 | }; 55 | 56 | 57 | module.exports = exportObj; -------------------------------------------------------------------------------- /server/routes/webRTC_routes.js: -------------------------------------------------------------------------------- 1 | var dbHelpers = require('../database_queries/database_queries.js'); 2 | 3 | module.exports = function(app) { 4 | app.post('/users', function(req, res) { 5 | var packet = req.body; 6 | if (packet.userId) { 7 | console.log('SENDER post event'); 8 | dbHelpers.addLink(packet.hash, packet.userId) 9 | .then(function(result) { 10 | res.status(201); 11 | res.send('link added'); 12 | }); 13 | } else { 14 | console.log('RECEIVER post response event'); 15 | dbHelpers.addReceiverToSender(packet.hash, packet.recipientId) 16 | .then(function(result) { 17 | console.log('adding receiverToSender'); 18 | dbHelpers.getSenderId(packet.hash) 19 | .then(function(result) { 20 | res.status(201); 21 | res.send(result); 22 | }); 23 | }); 24 | } 25 | 26 | app.post('/deleteReceiverId', function(req, res) { 27 | console.log('HASH', req.body.hash); 28 | dbHelpers.removeReceiverFromSender(req.body.hash, req.body.id).then(function(result) { 29 | console.log("deleted:", result, "from sender"); 30 | res.status(201); 31 | res.send(result); 32 | }); 33 | }); 34 | 35 | app.post('/deleteSenderObject', function(req, res) { 36 | dbHelpers.deleteLink(req.body.userId).then(function(result) { 37 | res.status(201); 38 | res.send(result); 39 | }); 40 | }); 41 | 42 | // TODO: store caller userId, somehow 43 | // tie to random link generated 44 | }); 45 | 46 | app.get('/users', function(req, res) { 47 | res.status(200); 48 | res.send(); 49 | }); 50 | 51 | // When someone accesses one of the created links, 52 | // make a request here for callee to recieve the caller userId 53 | }; 54 | -------------------------------------------------------------------------------- /spec/factory_tests/fileUploadFactory_spec.js: -------------------------------------------------------------------------------- 1 | describe('fileUpload Factory', function() { 2 | var fileUpload; 3 | beforeEach(function() { 4 | module('utils'); //angular.module name 5 | 6 | inject(function($injector) { 7 | fileUpload = $injector.get('fileUpload'); 8 | }); 9 | }); 10 | 11 | describe('fileUpload', function() { 12 | describe('convertFileSize', function() { 13 | it('should return in GB for files larger than 1000000000 bytes', function() { 14 | var testGB = fileUpload.convertFileSize(438290423894); 15 | var result = testGB.substring(testGB.length - 2); 16 | expect(result).toBe('GB'); 17 | }); 18 | 19 | it('should return in MB for files larger than 1000000 bytes', function() { 20 | var testMB = fileUpload.convertFileSize(290423894); 21 | var result = testMB.substring(testMB.length - 2); 22 | expect(result).toBe('MB'); 23 | }); 24 | 25 | it('should return in kB for files less than 1000000 bytes', function() { 26 | var testkB = fileUpload.convertFileSize(423894); 27 | var result = testkB.substring(testkB.length - 2); 28 | expect(result).toBe('kB'); 29 | }); 30 | }); 31 | 32 | describe('getTransferRate', function() { 33 | var transferInfo; 34 | 35 | beforeEach(function() { 36 | var transferObj = { 37 | nextTime: Date.now() - 1000, 38 | stored: 0, 39 | size: 100000, 40 | progress: 10000 41 | }; 42 | transferInfo = fileUpload.getTransferRate(transferObj); 43 | }); 44 | 45 | it('should return an object', function() { 46 | expect(transferInfo.constructor).toBe(Object); 47 | }); 48 | 49 | it('should have transfer rate', function() { 50 | expect(transferInfo.rate).toBe('10.00 kB/s'); 51 | }); 52 | 53 | it('should have time remaining', function() { 54 | expect(transferInfo.time).toBe('00:09'); 55 | }); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /client/app/components/client_utilities/factories/linkGenerationFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('utils.linkGeneration', []) 2 | 3 | .factory('linkGeneration', [function() { 4 | var linkGeneration = {}; 5 | 6 | var s4 = function() { 7 | //this is a random 4-charactder hash generator 8 | return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 9 | }; 10 | 11 | linkGeneration.adjAdjAnimal = function() { 12 | // global function from adjective-adjective-animal bower package, returns promise 13 | return adjAdjAnimal(); 14 | }; 15 | 16 | linkGeneration.generateHash = function() { 17 | // 16 character hash 18 | return s4() + s4() + s4() + s4(); 19 | }; 20 | 21 | linkGeneration.copyToClipboard = function(elem) { 22 | // create hidden text element, if it doesn't already exist 23 | var targetId = "_hiddenCopyText_"; 24 | var isInput = elem.tagName === "INPUT" || elem.tagName === "TEXTAREA"; 25 | var origSelectionStart, origSelectionEnd, target; 26 | if (isInput) { 27 | // can just use the original source element for the selection and copy 28 | target = elem; 29 | origSelectionStart = elem.selectionStart; 30 | origSelectionEnd = elem.selectionEnd; 31 | } else { 32 | // must use a temporary form element for the selection and copy 33 | target = document.getElementById(targetId); 34 | if (!target) { 35 | target = document.createElement("textarea"); 36 | target.style.position = "absolute"; 37 | target.style.left = "-9999px"; 38 | target.style.top = "0"; 39 | target.id = targetId; 40 | document.body.appendChild(target); 41 | } 42 | target.textContent = elem.textContent; 43 | } 44 | // select the content 45 | var currentFocus = document.activeElement; 46 | target.focus(); 47 | target.setSelectionRange(0, target.value.length); 48 | 49 | // copy the selection 50 | return document.execCommand("copy"); 51 | } 52 | 53 | return linkGeneration; 54 | 55 | }]); 56 | -------------------------------------------------------------------------------- /client/app/components/download/downloadView.html: -------------------------------------------------------------------------------- 1 |
Incoming Files 2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 |
File Name: 10 |
{{transfer.name}}
11 |
{{transfer.type}}
12 |
File Size: 13 |
{{transfer.formattedSize}}
14 |
15 | 16 |
{{transfer.percent}}
17 | 18 |
19 |
20 |
21 | {{transfer.rate}}{{transfer.time}} 22 |
23 | 24 | Download 25 | 26 |
27 |
28 | 29 |
30 |
File Name: 31 |
{{offer.name}}
32 |
File Size: 33 |
{{offer.size}}
34 |
35 |
Would you like to accept the file?
36 |
37 | 38 | 39 |
40 |
41 | 42 |
43 | 44 |
45 | 46 | -------------------------------------------------------------------------------- /client/app/components/client_utilities/factories/lightningButtonFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('utils.lightningButton', []) 2 | 3 | .factory('lightningButton', ['linkGeneration', function(linkGeneration){ 4 | var lightningButton = {}; 5 | 6 | var savedClasses = 'btn btn-circle lightningHover'; 7 | lightningButton.activateLightningButton = function(){ 8 | 9 | $('#lightningBoltButton').mouseenter(function() { 10 | savedClasses = $('#lightningBoltButton').attr('class'); 11 | $('#lightningBoltButton').attr('class', 'btn btn-circle lightningHover'); 12 | }); 13 | 14 | $('#lightningBoltButton').mouseleave(function() { 15 | $('#lightningBoltButton').attr('class', savedClasses); 16 | savedClasses = 'btn btn-circle lightningHover'; 17 | }); 18 | 19 | $('#lightningBoltButton').mousedown(function() { 20 | $('#lightningBoltButton').addClass('clicked'); 21 | }); 22 | 23 | $('#lightningBoltButton').mouseup(function() { 24 | $('#lightningBoltButton').removeClass('clicked'); 25 | }); 26 | 27 | }; 28 | 29 | lightningButton.addLinkToLightningButton = function(){ 30 | $('#lightningBoltButton').on('click', function() { 31 | linkGeneration.copyToClipboard(document.getElementById("currentUrl")); 32 | if (!savedClasses.includes('connectedToPeer')) { 33 | if (window.location.href.includes('/room/')) { 34 | savedClasses = 'btn btn-circle lightningHover waitingForConnection'; 35 | } 36 | } 37 | }); 38 | }; 39 | 40 | lightningButton.connectedToPeer = function(){ 41 | $('#lightningBoltButton').removeClass('waitingForConnection'); 42 | $('#lightningBoltButton').addClass('connectedToPeer'); 43 | $('.currentConnectionState').text('Connected!'); 44 | }; 45 | 46 | lightningButton.disconnected = function(){ 47 | $('#lightningBoltButton').removeClass('connectedToPeer'); 48 | $('#lightningBoltButton').addClass('disconnected'); 49 | $('.currentConnectionState').text('Disconnected'); 50 | }; 51 | 52 | lightningButton.awaitingConnection = function(){ 53 | $('#lightningBoltButton').addClass('waitingForConnection'); 54 | $('.currentConnectionState').text('Awaiting Connection...'); 55 | }; 56 | 57 | return lightningButton; 58 | }]); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu Jan 21 2016 19:33:45 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: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'bower_components/angular/angular.js', 19 | 'bower_components/angular-mocks/angular-mocks.js', 20 | 'bower_components/angular-resource/angular-resource.js', 21 | 'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js', 22 | 'bower_components/angular-ui-notification/dist/angular-ui-notification.min.js', 23 | 'client/lib/nochunkbufferfixpeer.min.js', 24 | 'client/app/**/*.js', 25 | 'spec/factory_tests/**/*.js', 26 | 'spec/controller_tests/**/*.js' 27 | ], 28 | 29 | 30 | // list of files to exclude 31 | exclude: [], 32 | 33 | 34 | // preprocess matching files before serving them to the browser 35 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 36 | preprocessors: {}, 37 | 38 | 39 | // test results reporter to use 40 | // possible values: 'dots', 'progress' 41 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 42 | reporters: ['nyan'], 43 | 44 | 45 | // web server port 46 | port: 9876, 47 | 48 | 49 | // enable / disable colors in the output (reporters and logs) 50 | colors: true, 51 | 52 | 53 | // level of logging 54 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 55 | logLevel: config.LOG_INFO, 56 | 57 | 58 | // enable / disable watching file and executing tests whenever any file changes 59 | autoWatch: true, 60 | 61 | 62 | // start these browsers 63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 64 | browsers: ['Chrome'], 65 | 66 | 67 | // Continuous Integration mode 68 | // if true, Karma captures browsers, runs the tests and exits 69 | singleRun: true, 70 | 71 | // Concurrency level 72 | // how many browser should be started simultaneous 73 | concurrency: Infinity 74 | }); 75 | }; -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | 4 | jshint: { 5 | files: ['*.js', 'client/app/**/*.js', 'server/**/*.js', 'database/**/*.js', '*.json', 'spec/**/*.js'], 6 | options: { 7 | ignores: [ 8 | 9 | ] 10 | } 11 | }, 12 | 13 | uglify: { 14 | target: { 15 | files: { 16 | // These need to be uglified in specific order, app files last. 17 | 'client/allmincode.js': [ 18 | 'bower_components/adjective-adjective-animal/dist/adjective-adjective-animal.min.js', 19 | 'bower_components/angular/angular.min.js', 20 | 'bower_components/angular-ui-router/release/angular-ui-router.min.js', 21 | 'bower_components/angular-animate/angular-animate.min.js', 22 | 'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js', 23 | 'client/lib/nochunkbufferfixpeer.min.js', 24 | 'bower_components/localforage/dist/localforage.min.js', 25 | 'bower_components/jquery/dist/jquery.min.js', 26 | 'bower_components/angular-ui-notification/dist/angular-ui-notification.min.js', 27 | 'bower_components/bootstrap/dist/js/bootstrap.min.js', 28 | 'client/app/**/*.js' 29 | ] 30 | } 31 | } 32 | 33 | }, 34 | 35 | cssmin: { 36 | target: { 37 | files: { 38 | 'client/assets/styles.min.css': [ 39 | 'bower_components/angular-ui-notification/dist/angular-ui-notification.min.css', 40 | 'bower_components/font-awesome/css/font-awesome.min.css', 41 | 'bower_components/bootstrap/dist/css/bootstrap.min.css', 42 | 'client/assets/styles.css' 43 | ] 44 | } 45 | } 46 | }, 47 | 48 | watch: { 49 | files: ['client/app/**/*.js'], 50 | tasks: ['jshint'] 51 | } 52 | }); 53 | 54 | //Automatic desktop notifications for Grunt errors and warnings 55 | grunt.loadNpmTasks('grunt-notify'); 56 | grunt.loadNpmTasks('grunt-contrib-jshint'); 57 | grunt.loadNpmTasks('grunt-contrib-watch'); 58 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 59 | grunt.loadNpmTasks('grunt-contrib-uglify'); 60 | grunt.loadNpmTasks('grunt-contrib-concat'); 61 | 62 | /************************************************************* 63 | Run `$ grunt jshint` before submitting PR 64 | Or run `$ grunt` with no arguments to watch files 65 | **************************************************************/ 66 | 67 | grunt.registerTask('default', ['watch']); 68 | grunt.registerTask('minall', ['uglify', 'cssmin']); 69 | }; 70 | -------------------------------------------------------------------------------- /spec/server_spec.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var base_url = 'http://localhost:3000/'; 3 | var db = require('./../database/config.js'); 4 | var server = require('./../server/server.js'); 5 | var dbQueries = require('./../server/database_queries/database_queries.js'); 6 | var User = require('./../database/models/user_schema.js'); 7 | 8 | var testNumber = 1; 9 | 10 | beforeEach(function() { 11 | console.log('======Starting test #', testNumber); 12 | }); 13 | 14 | afterEach(function() { 15 | console.log('=======Finished test #', testNumber); 16 | testNumber++; 17 | }); 18 | 19 | 20 | describe("Server", function() { 21 | it("should respond with status code 200", function(done) { 22 | request(base_url, function(error, response, body) { 23 | expect(response.statusCode).toBe(200); 24 | done(); 25 | }); 26 | }); 27 | 28 | it("addLink should be able to add an item to the database", function(done) { 29 | dbQueries.addLink('Test Hash MKS', 'Test Sender MKS').then(function() { 30 | User.findOne({ 31 | linkHash: 'Test Hash MKS' 32 | }, function(err, user) { 33 | if (err) { 34 | return err; 35 | } 36 | }).then(function(result) { 37 | expect(result.senderID).toBe('Test Sender MKS'); 38 | done(); 39 | }); 40 | }); 41 | }); 42 | 43 | it("addReceiverToSender should be able to add a receiverID to a sender", function(done) { 44 | dbQueries.addReceiverToSender('Test Hash MKS', 'Test Receiver MKS').then(function() { 45 | User.findOne({ 46 | linkHash: 'Test Hash MKS' 47 | }, function(err, user) { 48 | if (err) { 49 | return err; 50 | } 51 | }).then(function(result) { 52 | console.log('result:', result); 53 | expect(result.receiverIDArray[result.receiverIDArray.length - 1]).toBe('Test Receiver MKS'); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | 59 | it("getSenderId be able to get user object from a link hash", function(done) { 60 | dbQueries.getSenderId('Test Hash MKS').then( 61 | function(result) { 62 | expect(result.senderID).toBe('Test Sender MKS'); 63 | done(); 64 | }); 65 | }); 66 | 67 | it("should be able to delete a particular receiverID from a sender", function(done) { 68 | dbQueries.removeReceiverFromSender('Test Hash MKS', 'Test Receiver MKS').then(function() { 69 | User.find({linkHash: 'Test Hash MKS'}, function(err, user) { 70 | if (err) { 71 | return err; 72 | } 73 | }).size('receiverIDArray', 0).then(function(result) { 74 | expect(result[0].senderID).toBe('Test Sender MKS'); 75 | done(); 76 | }); 77 | }); 78 | }); 79 | 80 | it("should be able to delete an item from the database", function(done) { 81 | dbQueries.deleteLink('Test Sender MKS').then(function() { 82 | User.findOne({ 83 | senderID: 'Test Sender MKS' 84 | }, function(err, user) { 85 | if (err) { 86 | return err; 87 | } 88 | }).then(function(result) { 89 | expect(result).toBe(null); 90 | done(); 91 | }); 92 | }); 93 | }); 94 | 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /client/app/components/home/homeController.js: -------------------------------------------------------------------------------- 1 | angular.module('home', [ 2 | 'utils' 3 | ]) 4 | 5 | .controller('homeController', [ 6 | '$scope', 7 | '$http', 8 | '$state', 9 | '$stateParams', 10 | '$location', 11 | '$rootScope', 12 | 'fileTransfer', 13 | 'linkGeneration', 14 | 'webRTC', 15 | 'packetHandlers', 16 | 'fileUpload', 17 | 'modals', 18 | 'notifications', 19 | 'lightningButton', 20 | function($scope, $http, $state, $stateParams, $location, $rootScope, fileTransfer, linkGeneration, webRTC, packetHandlers, fileUpload, modals, notifications, lightningButton) { 21 | console.log('home controller loaded'); 22 | fileTransfer.myItems = []; 23 | fileTransfer.conn = []; 24 | 25 | var disconnectingSenderId = null; 26 | var generateLink = function() { 27 | $scope.hash = linkGeneration.adjAdjAnimal().then(function(val) { 28 | $scope.hash = val; 29 | $state.go('room', { 30 | roomHash: $scope.hash 31 | }); 32 | }); 33 | }; 34 | 35 | $rootScope.openModal = modals.openModal; 36 | fileUpload.checkBrowser(); 37 | 38 | 39 | 40 | document.getElementById('filesId').addEventListener('change', function() { 41 | fileUpload.checkBrowser(); 42 | $('#alertMessage').text('Click the bolt to copy the link to your clipboard'); 43 | var self = this; 44 | $rootScope.$apply(function() { 45 | fileUpload.receiveFiles.call(self); 46 | }); 47 | 48 | if (!fileTransfer.peer) { 49 | lightningButton.activateLightningButton(); 50 | lightningButton.awaitingConnection(); 51 | lightningButton.addLinkToLightningButton(); 52 | 53 | fileTransfer.peer = webRTC.createPeer(); 54 | console.log('SENDER peer created'); 55 | fileTransfer.peer.on('open', function(id) { 56 | disconnectingSenderId = id; 57 | $http({ 58 | method: 'POST', 59 | url: '/api/webrtc/users', 60 | data: { 61 | userId: id, 62 | hash: $scope.hash 63 | } 64 | }) 65 | .then(function(result) { 66 | console.log('SENDER\'s POST response', result.data); 67 | notifications.tabReminder(); 68 | }); 69 | }); 70 | fileTransfer.peer.on('connection', function(conn) { 71 | fileTransfer.conn.push(conn); 72 | lightningButton.connectedToPeer(); 73 | 74 | conn.on('open', function() { 75 | fileTransfer.conn.forEach(function(connection) { 76 | webRTC.clearQueue(fileTransfer.myItems, connection); 77 | }); 78 | }); 79 | packetHandlers.attachConnectionListeners(conn, $rootScope); 80 | }); 81 | generateLink(); 82 | } 83 | 84 | window.onbeforeunload = function(e) { 85 | e.preventDefault(); 86 | //stops notification from showing 87 | }; 88 | 89 | window.addEventListener('beforeunload', function() { 90 | fileTransfer.peer.destroy(); 91 | $http({ 92 | method: 'POST', 93 | url: '/api/webrtc/deleteSenderObject', 94 | data: { 95 | userId: disconnectingSenderId 96 | } 97 | }); 98 | }); 99 | 100 | }); 101 | 102 | 103 | }]); 104 | -------------------------------------------------------------------------------- /client/app/components/connecting/connectingController.js: -------------------------------------------------------------------------------- 1 | angular.module('connecting', [ 2 | 'utils' 3 | ]) 4 | 5 | .controller('connectingController', [ 6 | '$scope', 7 | '$http', 8 | '$stateParams', 9 | '$rootScope', 10 | 'fileTransfer', 11 | 'webRTC', 12 | 'packetHandlers', 13 | 'fileUpload', 14 | 'modals', 15 | 'linkGeneration', 16 | 'lightningButton', 17 | 'notifications', 18 | function($scope, $http, $stateParams, $rootScope, fileTransfer, webRTC, packetHandlers, fileUpload, modals, linkGeneration, lightningButton, notifications) { 19 | console.log('connecting controller loaded'); 20 | /** 21 | * if arriving from redirect, 22 | * sender has access to their own peer object, 23 | * becasue it's on the fileTransfer 24 | * 25 | * if arriving from a link, 26 | * follow the code below: 27 | */ 28 | 29 | fileUpload.checkBrowser(); 30 | 31 | 32 | $rootScope.openModal = modals.openModal; 33 | 34 | 35 | $('.currentUrlShow').removeClass('currentUrlHidden'); 36 | 37 | setTimeout(function() { 38 | currentUrl.innerHTML = window.location.href; 39 | 40 | }, 0); 41 | 42 | var disconnectingReceiverId = null; 43 | 44 | $scope.incomingFileTransfers = fileTransfer.incomingFileTransfers; 45 | $scope.outgoingFileTransfers = fileTransfer.outgoingFileTransfers; 46 | $scope.acceptFileOffer = fileUpload.acceptFileOffer; 47 | $scope.rejectFileOffer = fileUpload.rejectFileOffer; 48 | $scope.offers = fileTransfer.offers; 49 | console.log('connecting scope', $scope.offers); 50 | 51 | if (!fileTransfer.peer) { 52 | lightningButton.activateLightningButton(); 53 | lightningButton.awaitingConnection(); 54 | fileTransfer.myItems = []; 55 | 56 | fileTransfer.conn = []; 57 | 58 | fileTransfer.peer = webRTC.createPeer(); 59 | 60 | fileTransfer.peer.on('open', function(id) { 61 | disconnectingReceiverId = id; 62 | $('.currentConnectionState').text('Connecting...'); 63 | $http({ 64 | method: 'POST', 65 | url: '/api/webrtc/users', 66 | data: { 67 | hash: $stateParams.roomHash, 68 | recipientId: id 69 | } 70 | }) 71 | .then(function(res) { 72 | // expect res.data === sender id 73 | var conn = fileTransfer.peer.connect(res.data.senderID); 74 | fileTransfer.conn.push(conn); 75 | packetHandlers.attachConnectionListeners(conn, $rootScope); 76 | notifications.tabReminder(); 77 | conn.on('open', function(){ 78 | lightningButton.connectedToPeer(); 79 | }); 80 | }); 81 | }); 82 | 83 | window.onbeforeunload = function(e) { 84 | //stops notification from showing 85 | e.preventDefault(); 86 | }; 87 | 88 | window.addEventListener('beforeunload', function() { 89 | fileTransfer.peer.destroy(); 90 | $http({ 91 | method: 'POST', 92 | url: '/api/webrtc/deleteReceiverId', 93 | data: { 94 | hash: $stateParams.roomHash, 95 | id: disconnectingReceiverId 96 | } 97 | }); 98 | }); 99 | 100 | document.getElementById('filesId').addEventListener('change', function() { 101 | var self = this; 102 | $scope.$apply(function(){ 103 | fileUpload.receiveFiles.call(self); 104 | }) 105 | }); 106 | } 107 | 108 | } 109 | ]); 110 | -------------------------------------------------------------------------------- /client/app/components/client_utilities/factories/webRTCFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('utils.webRTC', ['utils.fileReader']) 2 | 3 | .factory('webRTC', ['$http', 'fileReader', 'fileTransfer', function($http, fileReader, fileTransfer) { 4 | /** 5 | * user uploaded file 6 | * retrieve file & convert it to binary 7 | **/ 8 | var webRTC = {}; 9 | 10 | webRTC.createPeer = function() { 11 | var peer = new Peer({  12 | host: 'mkstream.herokuapp.com', 13 | secure: true, 14 | port: 443, 15 | config: { 16 | 'iceServers': [ 17 | {url: 'stun:stun.l.google.com:19302'}, 18 | {url: 'stun:stun1.l.google.com:19302'}, 19 | {url: 'stun:stun2.l.google.com:19302'}, 20 | {url: 'stun:stun3.l.google.com:19302'}, 21 | {url: 'stun:stun4.l.google.com:19302'} 22 | ] 23 | }, 24 | debug: 3 25 | }); 26 | 27 | webRTC.heartBeat(peer); 28 | 29 | return peer; 30 | 31 | }; 32 | 33 | webRTC.heartBeat = function(peer) { 34 | var alive = true; 35 | var makeHeartbeat = function() { 36 | if (alive) { 37 | setTimeout(makeHeartbeat, 20000); 38 | if (peer.socket._wsOpen()) { 39 | peer.socket.send({type: 'HEARTBEAT'}); 40 | } 41 | } 42 | }; 43 | makeHeartbeat(); 44 | return { 45 | start: function() { 46 | alive = true; 47 | makeHeartbeat(); 48 | }, 49 | stop: function() { 50 | alive = false; 51 | } 52 | }; 53 | }; 54 | 55 | var chunker = function(details, name) { 56 | var chunkSize = 16384; 57 | var slice = details.file.slice(details.offset, details.offset + chunkSize); 58 | fileReader.readAsArrayBuffer(slice, details.scopeRef) 59 | .then(function(buff) { 60 | var packet = { 61 | chunk: buff, 62 | type: 'file-chunk', 63 | count: details.count, 64 | id: details.id 65 | }; 66 | if (details.count === 0) { 67 | packet.name = name; 68 | packet.size = details.size; 69 | } 70 | details.conn.send(packet); 71 | // console.log('BufferSize:', details.conn.bufferSize); 72 | details.count++; 73 | if (details.size > details.offset + chunkSize) { 74 | details.offset += chunkSize; 75 | if(details.conn.bufferSize > 1000){ 76 | // if buffer queue exceeds 1000, wait for user's client to process first 77 | window.setTimeout(function(details) { 78 | chunker(details); 79 | }, 150, details); 80 | } else { 81 | window.setTimeout(function(details) { 82 | chunker(details); 83 | }, 0, details); 84 | } 85 | } else { 86 | console.log('File finished sending!'); 87 | } 88 | }); 89 | }; 90 | 91 | webRTC.sendDataInChunks = function(conn, obj) { 92 | fileTransfer.outgoingFileTransfers[obj.id] = { 93 | progress: 0, 94 | max: obj.size, 95 | name: obj.name 96 | }; 97 | chunker({ 98 | id: obj.id, 99 | count: 0, 100 | offset: 0, 101 | size: obj.size, 102 | conn: conn, 103 | file: obj.file, 104 | scopeRef: obj.scopeRef 105 | }, obj.name); 106 | }; 107 | 108 | webRTC.clearQueue = function(files, conn){ 109 | for(var i = 0; i < files.length; i++){ 110 | if(!files[i].beenSent){ 111 | files[i].beenSent = true; 112 | conn.send({ 113 | name: files[i].name, 114 | size: files[i].size, 115 | fileKey: files[i].fileKey, 116 | type: 'file-offer' 117 | }); 118 | } 119 | } 120 | }; 121 | 122 | webRTC.checkDownloadQueue = function(){ 123 | var first = fileTransfer.downloadQueue[0]; 124 | if(fileTransfer.downloadQueue.length === 0){ 125 | console.log('download queue empty'); 126 | } 127 | else if(!first.sending){ 128 | first.sending = true; 129 | first.conn.send({ 130 | name: first.name, 131 | size: first.rawSize, 132 | fileKey: first.fileKey, 133 | type: 'start-transfer' 134 | }); 135 | } 136 | }; 137 | 138 | return webRTC; 139 | 140 | }]); 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![MKSTream](client/assets/logo_plain.png) 2 | 3 | 4 | 5 | MKStream allows people to send files directly to each other through a secured and anonymous data-channel established by WebRTC's peer-connection. You can use our service across multiple browsers and across multiple platforms with no plugins, no downloads, and no installs necessary. 6 | 7 | # How to use MKStream 8 | **1. Upload files |** Visit [MKStream](http://mk-stream.herokuapp.com/#/) and upload your file(s) by clicking the drop-zone and/or dragging and dropping a file into the drop-zone. 9 | 10 | **2. Share link |** After choosing your file(s), MKStream creates a anonymous and temporary unique url used to transfer files in the browser. Clicking on the glowing lightning bolt below the drop-zone will copy the temporary url to your clipboard. Give this link to another person to establish a connection and begin transfering. 11 | 12 | **3. Transfer data |** Your url and your file(s) will be available for transfer as long as you keep your MKStream tab open. When both people have the unique url loaded, a file will appear in your receiving section. Clicking accept on a file begins the transfer and readies it for you to download. While connected, feel free to transfer your files back and forth. 13 | 14 | **Note:** 15 | For anonymity and security reasons, either peer closing or refreshing their MKStream tab destroys the connection and voids the current anonymous link. To share more files, just go back to [MKStream](http://mk-stream.herokuapp.com/#/) and MKStream will be ready for you any time. 16 | 17 | 18 | ## How it works 19 | # ![MKSTream](client/assets/mkstream_architecture.png) 20 | 21 | When someone first shares a file, two things occur. First, a signal is sent to our PeerJS server to have it listen for another peer to connect with. Second, a unique link is generated and a signal is sent to our server to store the room in our database. 22 | 23 | When another user visits that link, it signals our server to check if the room exists in the database. If so, the second peer will signal the same PeerJS server and look for the connection to the peer that generated the link. Once a handshake is established, WebRTC will establish the direct data channel between the two peers. 24 | 25 | # Contributing 26 | **MKStream is brought to you by:** 27 | **Malek Ascha** | Product Owner & Software Engineer 28 | **Kevin Van** | Software Engineer 29 | **Simon Ding** | Software Engineer 30 | **Tyler Ferrier** | Scrum Master & Software Engineer 31 | 32 | # Getting Started 33 | [View our git workflow](https://github.com/MAKE-SITY/MKSTream/wiki/Git-Workflow) 34 | 35 | [View our commit styling guidelines](https://github.com/MAKE-SITY/MKSTream/wiki/Commit-Styling) 36 | 37 | # Installation 38 | Fork our repo and clone it from your own copy. Install all required node modules and bower components. 39 | 40 | `npm install` 41 | 42 | Bower components are included in the post install. 43 | 44 | # Usage 45 | 46 | After all dependencies are installed, run the application with: 47 | 48 | `npm start` 49 | 50 | While running the application, rooms will be saved to your local mongoDB unless you specify another mongoDB through an environment variable. To do so, create an file named ".env" in the root directory, and enter: 51 | 52 | MONGOLAB_URI='(your mongodb uri)' 53 | 54 | # Testing 55 | 56 | ## Executing tests 57 | 58 | `npm test` - Run client-side tests then server-side tests. 59 | 60 | To execute client and server tests separately, you may need to globally install some packages: 61 | 62 | `sudo npm install -g karma-cli` 63 | 64 | `sudo npm install -g jasmine` 65 | 66 | Executing tests individually: 67 | 68 | `karma start` - Run client side tests. 69 | 70 | `jasmine` - Run server side tests. 71 | 72 | # Grunt Scripts 73 | 74 | `grunt jshint` - Search files for lint. 75 | 76 | `grunt uglify` - Minify all required javascript. 77 | 78 | Output: /client/allmincode.js 79 | 80 | Automatically concats and minifies all required libraries and application code in specific order. See Gruntfile.js for details. 81 | 82 | NOTE: For development purposes, it is easier to comment back in all the bower components and comment out allmincode.js. When you are ready to finalize and deploy, run the uglify task again and use only allmincode.js to serve the smallest file to the user. 83 | 84 | `grunt cssmin` - output: /client/assets/styles.min.css 85 | 86 | Minifies local css only, excluding external css libraries such as bootstrap. 87 | 88 | `grunt minall` - Executes uglify and cssmin. 89 | 90 | `grunt watch` - Watches all files for lint during development. 91 | 92 | # Style Guide 93 | Access our [Style Guide here](https://github.com/MKSTeam/thesis/wiki/Style-Guide) 94 | 95 | # Press Release 96 | Access our [Press Release here](https://github.com/MKSTeam/thesis/wiki/Press-Release) 97 | -------------------------------------------------------------------------------- /client/app/components/client_utilities/factories/fileUploadFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('utils.fileUpload', ['utils.fileReader']) 2 | 3 | .factory('fileUpload', [ 4 | 'fileReader', 5 | 'fileTransfer', 6 | 'webRTC', 7 | 'linkGeneration', 8 | 'notifications', 9 | 'modals', 10 | function(fileReader, fileTransfer, webRTC, linkGeneration, notifications, modals) { 11 | var fileUpload = {}; 12 | 13 | fileUpload.getFiles = function() { 14 | return document.getElementById('filesId').files; 15 | }; 16 | 17 | fileUpload.convertFromBinary = function(data) { 18 | var kit = {}; 19 | kit.href = URL.createObjectURL(data.file); 20 | kit.name = data.name; 21 | kit.size = data.size; 22 | return kit; 23 | }; 24 | 25 | fileUpload.convertFileSize = function(num) { 26 | if (num > 1000000000) { 27 | return (num / 1000000000).toFixed(2) + ' GB'; 28 | } else if (num > 1000000) { 29 | return (num / 1000000).toFixed(2) + ' MB'; 30 | } else { 31 | return (num / 1000).toFixed(2) + ' kB'; 32 | } 33 | }; 34 | 35 | fileUpload.acceptFileOffer = function(offer) { 36 | fileTransfer.incomingFileTransfers[offer.fileKey] = { 37 | name: offer.name, 38 | size: offer.size, 39 | formattedSize: fileUpload.convertFileSize(offer.rawSize), 40 | progress: 0 41 | }; 42 | fileTransfer.downloadQueue.push(offer); 43 | var index = fileTransfer.offers.indexOf(offer); 44 | fileTransfer.offers.splice(index, 1); 45 | webRTC.checkDownloadQueue(); 46 | }; 47 | 48 | fileUpload.rejectFileOffer = function(offer) { 49 | var index = fileTransfer.offers.indexOf(offer); 50 | fileTransfer.offers.splice(index, 1); 51 | offer.conn.send({ 52 | type: 'file-rejected', 53 | fileKey: offer.fileKey 54 | }); 55 | }; 56 | 57 | 58 | var convertRate = function(rate) { 59 | // expects kB/s 60 | if (rate > 1000) { 61 | return (rate / 1000).toFixed(2).toString() + ' MB/s'; 62 | } else { 63 | return rate.toFixed(2).toString() + ' kB/s'; 64 | } 65 | }; 66 | 67 | var convertTime = function(timeInSeconds) { 68 | // expects seconds 69 | var sec_num = parseInt(timeInSeconds, 10); 70 | var hours = Math.floor(sec_num / 3600); 71 | var minutes = Math.floor((sec_num - (hours * 3600)) / 60); 72 | var seconds = sec_num - (hours * 3600) - (minutes * 60); 73 | if (hours < 10) { 74 | hours = "0" + hours; 75 | } 76 | if (minutes < 10) { 77 | minutes = "0" + minutes; 78 | } 79 | if (seconds < 10) { 80 | seconds = "0" + seconds; 81 | } 82 | var time = minutes + ':' + seconds; 83 | if (hours > 0) { 84 | time = hours + ':' + time; 85 | } 86 | return time; 87 | }; 88 | 89 | fileUpload.getTransferRate = function(transferObj) { 90 | // takes the incoming transferObj which is expected to have certain properties 91 | var currentTime = Date.now(); 92 | var timeToWait = 1000; // ms 93 | if (currentTime >= transferObj.nextTime) { 94 | transferObj.nextTime = Date.now() + timeToWait; 95 | var pastBytes = transferObj.stored; 96 | transferObj.stored = transferObj.progress; 97 | var rate = ((transferObj.stored - pastBytes)) / (timeToWait); // B/ms (kB/s) 98 | var maxFileSize = transferObj.size; 99 | timeRemaining = (maxFileSize - transferObj.stored) / rate / 1000; // ms/1000 -> s 100 | // console.log('CURRENT BYTES', transferObj.stored); 101 | // console.log('PASTCOUNT', pastBytes); 102 | // console.log('DIFFERENCE', transferObj.stored - pastBytes); 103 | // console.log('maxFileSize', maxFileSize); 104 | // console.log('REMAINING BYTES', maxFileSize - transferObj.stored); 105 | 106 | convertedRate = convertRate(rate); 107 | convertedTime = convertTime(timeRemaining); 108 | // console.log('RATE:', convertedRate); 109 | // console.log('TIME REMAINING:', convertedTime); 110 | 111 | } 112 | 113 | 114 | return { 115 | rate: convertedRate, 116 | time: convertedTime 117 | }; 118 | }; 119 | 120 | fileUpload.receiveFiles = function(){ 121 | var files = this.files; 122 | var alreadyUploaded; 123 | for (var i = 0; i < files.length; i++) { 124 | alreadyUploaded = false; 125 | for(var j = 0; j < fileTransfer.myItems.length; j++){ 126 | if(fileTransfer.myItems[j].name === files[i].name && fileTransfer.myItems[j].size === files[i].size){ 127 | alreadyUploaded = true; 128 | break; 129 | } 130 | } 131 | if (alreadyUploaded) { 132 | notifications.alreadyUploaded(files[i].name); 133 | continue; 134 | }; 135 | files[i].fileKey = linkGeneration.generateHash(); 136 | files[i].beenSent = false; 137 | files[i].formattedSize = fileUpload.convertFileSize(files[i].size); 138 | files[i].status = 'Waiting for response...'; 139 | fileTransfer.myItems.push(files[i]); 140 | } 141 | fileTransfer.conn.forEach(function(connection) { 142 | webRTC.clearQueue(fileTransfer.myItems, connection); 143 | }); 144 | }; 145 | 146 | fileUpload.checkBrowser = function(){ 147 | var goodBrowser = util.browser !== 'Unsupported'; 148 | var supportsDataChannel = util.supports.data; 149 | if(!goodBrowser || !supportsDataChannel){ 150 | modals.badBrowser(); 151 | } 152 | }; 153 | 154 | return fileUpload; 155 | 156 | }]); 157 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MKStream 7 | 8 | 9 | 10 | 11 | 12 | 26 | 27 | 28 | 40 | 41 | 42 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 107 | 108 | 109 | 110 |
111 |
112 | 113 |
114 | 115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | 127 |
drag and drop or click to start
128 | 129 |
130 |

A free, lightning fast file sharing service

131 | 132 |
133 | 134 |
135 |
136 |
137 |

138 |
Awaiting link generation
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /client/app/components/client_utilities/factories/packetHandlersFactory.js: -------------------------------------------------------------------------------- 1 | angular.module('utils.packetHandlers', ['utils.webRTC', 'utils.fileUpload', 'utils.linkGeneration']) 2 | 3 | 4 | .factory('packetHandlers', [ 5 | 'webRTC', 6 | 'fileUpload', 7 | 'linkGeneration', 8 | 'fileTransfer', 9 | 'notifications', 10 | 'lightningButton', 11 | function(webRTC, fileUpload, linkGeneration, fileTransfer, notifications, lightningButton) { 12 | var packetHandlers = {}; 13 | var fileNumber = 0; 14 | var fullArray = []; 15 | packetHandlers.startTransfer = function(data, conn, scope) { 16 | fileTransfer.myItems.forEach(function(val) { 17 | if (val.name === data.name && val.size === data.size) { 18 | var sendData = { 19 | file: val, 20 | name: data.name, 21 | size: data.size, 22 | id: data.fileKey, 23 | scopeRef: scope 24 | }; 25 | webRTC.sendDataInChunks(conn, sendData); 26 | for (var i = 0; i < fileTransfer.myItems.length; i++) { 27 | if (fileTransfer.myItems[i].name === data.name && fileTransfer.myItems[i].size === data.size) { 28 | fileTransfer.myItems[i].status = "Sending..."; 29 | } 30 | } 31 | } 32 | }); 33 | }; 34 | 35 | packetHandlers.offer = function(data, conn, scope) { 36 | scope.$apply(function() { 37 | var offer = { 38 | name: data.name, 39 | size: fileUpload.convertFileSize(data.size), 40 | conn: conn, 41 | fileKey: data.fileKey, 42 | rawSize: data.size 43 | }; 44 | fileTransfer.offers.push(offer); 45 | }); 46 | }; 47 | 48 | packetHandlers.chunk = function(data, conn, scope) { 49 | if (data.count === 0) { 50 | scope.$apply(function() { 51 | fileTransfer.incomingFileTransfers[data.id] = { 52 | buffer: {}, 53 | id: data.id, 54 | name: data.name, 55 | size: data.size, 56 | formattedSize: fileUpload.convertFileSize(data.size), 57 | progress: 0, 58 | fileNumber: fileNumber, 59 | chunkCount: 0, 60 | // used for transfer rate 61 | stored: 0, 62 | nextTime: Date.now() 63 | }; 64 | }); 65 | fileNumber++; 66 | } 67 | var blockSize = 5000; 68 | var transferObj = fileTransfer.incomingFileTransfers[data.id]; 69 | var blockIndex = Math.floor(data.count/blockSize); 70 | var relativeIndex = data.count % blockSize; 71 | if(!transferObj.buffer[blockIndex]){ 72 | transferObj.buffer[blockIndex] = []; 73 | transferObj.buffer[blockIndex].chunksReceived = 0; 74 | } 75 | var block = transferObj.buffer[blockIndex]; 76 | block[relativeIndex] = data.chunk; 77 | block.chunksReceived++; 78 | scope.$apply(function() { 79 | transferObj.progress += 16384; 80 | transferObj.rate = fileUpload.getTransferRate(transferObj).rate; 81 | transferObj.time = fileUpload.getTransferRate(transferObj).time; 82 | if (transferObj.progress > transferObj.size) { 83 | transferObj.progress = transferObj.size; 84 | transferObj.rate = 0.00; 85 | } 86 | transferObj.percent = (transferObj.progress/transferObj.size*100).toFixed(2).toString() + '%'; 87 | }); 88 | 89 | if (transferObj.progress >= transferObj.size) { 90 | var lastBlob = new Blob(block); 91 | block = transferObj.buffer[blockIndex] = null; 92 | 93 | localforage.setItem(data.id + ':' + transferObj.chunkCount.toString(), lastBlob) 94 | .then( 95 | function(result) { 96 | transferObj.chunkCount++; 97 | localforage.iterate(function(value, key, iterationNumber) { 98 | if (key.startsWith(data.id)) { 99 | fullArray[key.split(':')[1]] = value; 100 | // delete document after appending 101 | localforage.removeItem(key); 102 | console.log('Removed key:', key); 103 | } 104 | // clear this document from db after 105 | }, function(err) { 106 | if (err) { 107 | console.log('Error iterating through db!:', err); 108 | } else { 109 | console.log('localforage iteration completed'); 110 | } 111 | }) 112 | .then(function() { 113 | // console.log('all promise resolved'); 114 | var newFile = fileUpload.convertFromBinary({ 115 | file: new Blob(fullArray), 116 | name: transferObj.name, 117 | size: transferObj.size 118 | }); 119 | fullArray = []; 120 | 121 | fileTransfer.finishedTransfers.push(newFile); 122 | var downloadAnchor = document.getElementById('file' + transferObj.fileNumber); 123 | downloadAnchor.download = newFile.name; 124 | downloadAnchor.href = newFile.href; 125 | notifications.successMessage(newFile.name); 126 | fileTransfer.downloadQueue.shift(); 127 | webRTC.checkDownloadQueue(); 128 | conn.send({ 129 | fileKey: data.id, 130 | type: 'file-finished' 131 | }); 132 | }); 133 | } 134 | ); 135 | } else if (block.chunksReceived === blockSize) { 136 | // this code takes the data off browser memory and stores to user's temp storage 137 | console.log('saved block at ', blockSize, 'chunks'); 138 | var blobChunk = new Blob(block); 139 | block = transferObj.buffer[blockIndex] = null; 140 | localforage.setItem(data.id + ':' + transferObj.chunkCount.toString(), blobChunk); 141 | transferObj.chunkCount++; 142 | } 143 | }; 144 | 145 | packetHandlers.finished = function(data, scope){ 146 | for (var j = 0; j < fileTransfer.myItems.length; j++) { 147 | if (fileTransfer.myItems[j].fileKey === data.fileKey) { 148 | scope.$apply(function() { 149 | fileTransfer.myItems[j].status = 'File finished sending'; 150 | }); 151 | } 152 | } 153 | }; 154 | 155 | packetHandlers.rejected = function(data, scope){ 156 | for (var j = 0; j < fileTransfer.myItems.length; j++) { 157 | if (fileTransfer.myItems[j].fileKey === data.fileKey) { 158 | scope.$apply(function() { 159 | fileTransfer.myItems[j].status = 'File rejected'; 160 | }); 161 | } 162 | } 163 | }; 164 | 165 | packetHandlers.attachConnectionListeners = function(conn, scope){ 166 | conn.on('data', function(data) { 167 | // console.log('incoming packet'); 168 | if (data.type === 'start-transfer') { 169 | packetHandlers.startTransfer(data, conn, scope); 170 | } else if (data.type === 'file-offer') { 171 | packetHandlers.offer(data, conn, scope); 172 | } else if (data.type === 'file-chunk') { 173 | packetHandlers.chunk(data, conn, scope); 174 | } else if (data.type === 'file-finished') { 175 | packetHandlers.finished(data, scope); 176 | } else if (data.type === 'file-rejected') { 177 | packetHandlers.rejected(data, scope); 178 | } 179 | }); 180 | 181 | conn.on('error', function(err){ 182 | console.log('connection error: ', err); 183 | }); 184 | 185 | conn.on('close', function(){ 186 | notifications.connectionLost(); 187 | lightningButton.disconnected(); 188 | }); 189 | }; 190 | 191 | return packetHandlers; 192 | 193 | }]); 194 | -------------------------------------------------------------------------------- /client/assets/styles.css: -------------------------------------------------------------------------------- 1 | .intro, 2 | body, 3 | html { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | .navbar-custom { 9 | background: rgba(0, 0, 0, 0.2); 10 | } 11 | 12 | body { 13 | font-family: Lora, "Helvetica Neue", Helvetica, Arial, sans-serif; 14 | color: #fff; 15 | } 16 | 17 | .btn, 18 | .navbar-custom, 19 | h6 { 20 | font-family: Montserrat, "Helvetica Neue", Helvetica, Arial, sans-serif; 21 | } 22 | 23 | h6 { 24 | margin: 0; 25 | font-weight: 700; 26 | letter-spacing: 1px; 27 | } 28 | 29 | a { 30 | color: #42dca3; 31 | } 32 | 33 | a:focus, 34 | a:hover { 35 | text-decoration: none; 36 | color: #1d9b6c; 37 | } 38 | 39 | .light { 40 | font-weight: 400 41 | } 42 | 43 | .navbar-custom { 44 | margin-bottom: 0; 45 | border-bottom: 1px solid rgba(255, 255, 255, .3) 46 | } 47 | 48 | .navbar-custom .nav li a:active, 49 | .navbar-custom .nav li a:focus, 50 | .navbar-custom .nav li a:hover { 51 | background-color: transparent; 52 | outline: 0 53 | } 54 | 55 | .navbar-custom .navbar-brand { 56 | font-weight: 700 57 | } 58 | 59 | .navbar-custom .navbar-brand:focus { 60 | outline: 0 61 | } 62 | 63 | .navbar-custom a { 64 | color: #fff 65 | } 66 | 67 | .navbar-custom .navbar-brand:hover, 68 | .navbar-custom .nav li a:hover { 69 | /*color: rgba(255, 255, 255, .8);*/ 70 | color: #b266ff; 71 | opacity: 0.8; 72 | } 73 | 74 | .btn-circle, 75 | .intro, 76 | .navbar-custom .nav li.active a:hover { 77 | color: #fff 78 | } 79 | 80 | .fa { 81 | color: white; 82 | } 83 | 84 | .lightningHover { 85 | background-color: #e6e6e6; 86 | -webkit-box-shadow: 0 0 9px #fff; 87 | } 88 | 89 | .clicked { 90 | background-color: black; 91 | } 92 | 93 | .navbar-custom .nav li.active { 94 | outline: 0 95 | } 96 | 97 | 98 | /* purple highlight*/ 99 | 100 | .navbar-custom .nav li.active a { 101 | background-color: #a64dff; 102 | } 103 | 104 | .intro { 105 | display: table; 106 | height: auto; 107 | padding: 100px 0; 108 | text-align: center; 109 | background: url(./bright.jpg) bottom center no-repeat #000; 110 | -webkit-background-size: cover; 111 | -moz-background-size: cover; 112 | background-size: cover; 113 | -o-background-size: cover; 114 | -webkit-animation: fadein 2s; 115 | -moz-animation: fadein 2s; 116 | -ms-animation: fadein 2s; 117 | -o-animation: fadein 2s; 118 | animation: fadein 2s 119 | } 120 | 121 | .intro .intro-body { 122 | display: table-cell; 123 | vertical-align: middle 124 | } 125 | 126 | .fading-in { 127 | -webkit-animation: fadein 2s; 128 | -moz-animation: fadein 2s; 129 | -ms-animation: fadein 2s; 130 | -o-animation: fadein 2s; 131 | animation: fadein 2s 132 | } 133 | 134 | @keyframes fadein { 135 | from { 136 | opacity: 0 137 | } 138 | to { 139 | opacity: 1 140 | } 141 | } 142 | 143 | @-moz-keyframes fadein { 144 | from { 145 | opacity: 0 146 | } 147 | to { 148 | opacity: 1 149 | } 150 | } 151 | 152 | @-webkit-keyframes fadein { 153 | from { 154 | opacity: 0 155 | } 156 | to { 157 | opacity: 1 158 | } 159 | } 160 | 161 | @-ms-keyframes fadein { 162 | from { 163 | opacity: 0 164 | } 165 | to { 166 | opacity: 1 167 | } 168 | } 169 | 170 | @-o-keyframes fadein { 171 | from { 172 | opacity: 0 173 | } 174 | to { 175 | opacity: 1 176 | } 177 | } 178 | 179 | .pointer-class { 180 | cursor: pointer; 181 | } 182 | 183 | .currentUrlHidden { 184 | opacity: .01 185 | } 186 | 187 | #dropZone:hover, 188 | #fileLink { 189 | opacity: 1 190 | } 191 | 192 | @media(min-width:768px) { 193 | .intro { 194 | height: 100%; 195 | padding: 0 196 | } 197 | .intro .intro-body .brand-heading { 198 | font-size: 100px; 199 | } 200 | .intro .intro-body .intro-text { 201 | font-size: 26px 202 | } 203 | } 204 | 205 | .btn-circle { 206 | width: 70px; 207 | height: 70px; 208 | margin-top: 15px; 209 | padding: 7px 16px; 210 | border: 2px solid #fff; 211 | border-radius: 100%!important; 212 | font-size: 40px; 213 | } 214 | 215 | @-webkit-keyframes pulse { 216 | 0%, 217 | 100% { 218 | -webkit-transform: scale(1); 219 | transform: scale(1) 220 | } 221 | 50% { 222 | -webkit-transform: scale(1.2); 223 | transform: scale(1.2) 224 | } 225 | } 226 | 227 | @-moz-keyframes pulse { 228 | 0%, 229 | 100% { 230 | -moz-transform: scale(1); 231 | transform: scale(1) 232 | } 233 | 50% { 234 | -moz-transform: scale(1.2); 235 | transform: scale(1.2) 236 | } 237 | } 238 | 239 | .content-section { 240 | padding-top: 100px 241 | } 242 | 243 | .download-section { 244 | width: 100%; 245 | padding: 50px 0; 246 | color: #fff; 247 | background: url(./purple1.jpg) center center no-repeat #000; 248 | -webkit-background-size: cover; 249 | -moz-background-size: cover; 250 | background-size: cover; 251 | -o-background-size: cover 252 | } 253 | 254 | @media(min-width:767px) { 255 | .content-section { 256 | padding-top: 250px 257 | } 258 | .download-section { 259 | padding: 100px 0 260 | } 261 | #map { 262 | height: 400px; 263 | margin-top: 250px 264 | } 265 | } 266 | 267 | .btn { 268 | border-radius: 0; 269 | font-weight: 400; 270 | } 271 | 272 | #dropZone, 273 | #fileLink { 274 | border-radius: 10px; 275 | transition: all .5s ease 0s 276 | } 277 | 278 | ul.banner-social-buttons { 279 | margin-top: 0 280 | } 281 | 282 | @media(max-width:1199px) { 283 | ul.banner-social-buttons { 284 | margin-top: 15px 285 | } 286 | } 287 | 288 | @media(max-width:767px) { 289 | ul.banner-social-buttons li { 290 | display: block; 291 | margin-bottom: 20px; 292 | padding: 0 293 | } 294 | ul.banner-social-buttons li:last-child { 295 | margin-bottom: 0 296 | } 297 | } 298 | 299 | footer { 300 | padding: 50px 0 301 | } 302 | 303 | footer p { 304 | margin: 0 305 | } 306 | 307 | ::-moz-selection { 308 | text-shadow: none; 309 | background: #fcfcfc; 310 | background: rgba(255, 255, 255, .2) 311 | } 312 | 313 | ::selection { 314 | text-shadow: none; 315 | background: #fcfcfc; 316 | background: rgba(255, 255, 255, .2) 317 | } 318 | 319 | img::selection { 320 | background: 0 0 321 | } 322 | 323 | img::-moz-selection { 324 | background: 0 0 325 | } 326 | 327 | /*File Input Styles*/ 328 | 329 | #filesId { 330 | width: 100%; 331 | height: 100%; 332 | opacity: 0; 333 | position: relative; 334 | cursor: pointer; 335 | z-index: 101; 336 | } 337 | 338 | #dropZone { 339 | opacity: .6; 340 | position: relative; 341 | height: 100px; 342 | background-color: #fff; 343 | color: #000; 344 | border-width: 3px; 345 | border-style: dotted; 346 | margin-top: 15%; 347 | perspective: 500px; 348 | } 349 | 350 | #dropZoneText { 351 | position: relative; 352 | top: -55%; 353 | z-index: 100; 354 | } 355 | 356 | #fileLink { 357 | color: #fff; 358 | background-color: gray 359 | } 360 | 361 | #fileLink:hover { 362 | opacity: .9; 363 | transform: scale(1.01, 1.01) 364 | } 365 | 366 | /*Lightning button styles*/ 367 | 368 | @-webkit-keyframes yellowPulse { 369 | from, 370 | to { 371 | background-color: #cbcc00; 372 | -webkit-box-shadow: 0 0 9px #fff 373 | } 374 | 65% { 375 | background-color: #e5e600; 376 | -webkit-box-shadow: 0 0 px #91bd09 377 | } 378 | } 379 | 380 | @-webkit-keyframes greenPulse { 381 | from, 382 | to { 383 | background-color: #00b300; 384 | -webkit-box-shadow: 0 0 9px #fff 385 | } 386 | 65% { 387 | background-color: #38e600; 388 | -webkit-box-shadow: 0 0 px #91bd09 389 | } 390 | } 391 | 392 | @-webkit-keyframes redPulse { 393 | from, 394 | to { 395 | background-color: #ff3333; 396 | -webkit-box-shadow: 0 0 9px #fff 397 | } 398 | 65% { 399 | background-color: #e60000; 400 | -webkit-box-shadow: 0 0 px #91bd09 401 | } 402 | } 403 | 404 | .waitingForConnection { 405 | -webkit-animation-name: yellowPulse; 406 | -webkit-animation-duration: 1.5s; 407 | -webkit-animation-iteration-count: infinite; 408 | } 409 | 410 | .connectedToPeer { 411 | -webkit-animation-name: greenPulse; 412 | -webkit-animation-duration: 1.5s; 413 | -webkit-animation-iteration-count: infinite; 414 | } 415 | 416 | .disconnected { 417 | -webkit-animation-name: redPulse; 418 | -webkit-animation-duration: 1.5s; 419 | -webkit-animation-iteration-count: infinite; 420 | } 421 | 422 | .table-data-name { 423 | text-align: left; 424 | } 425 | 426 | .header-text { 427 | font-size: 1.875em; 428 | } 429 | 430 | .header-bar { 431 | border: none; 432 | height: 3px; 433 | color: #fff; 434 | background-color: #fff; 435 | } 436 | 437 | .table-data { 438 | word-wrap: break-word; 439 | text-align: center; 440 | } 441 | 442 | .table-data-type, 443 | .table-data-name { 444 | text-align: left; 445 | word-wrap: break-word; 446 | } 447 | 448 | .table-data-left { 449 | /*word-wrap: break-word;*/ 450 | text-align: left; 451 | } 452 | 453 | .table-data-right { 454 | /*word-wrap: break-word;*/ 455 | text-align: right; 456 | } 457 | 458 | progress[value] { 459 | width: 100%; 460 | position: relative; 461 | top: 2px; 462 | } 463 | 464 | .accept-request { 465 | text-align: left; 466 | font-size: 0.875em; 467 | } 468 | 469 | .data-acceptbtn, 470 | .data-rejectbtn { 471 | cursor: pointer; 472 | } 473 | 474 | .data-acceptbtn:hover { 475 | cursor: pointer; 476 | } 477 | 478 | .scroll-dl { 479 | height: 400px; 480 | overflow: auto; 481 | } 482 | 483 | .wrapped-dl { 484 | background: rgba(0, 0, 0, 0.5); 485 | /*background: rgba(0, 0, 0, 0.5);*/ 486 | border-radius: 5px; 487 | } 488 | 489 | .wrapped-ul { 490 | background: rgba(0, 0, 0, 0.5); 491 | border-radius: 5px; 492 | } 493 | 494 | 495 | /* The starting CSS styles for the enter animation for loading views*/ 496 | 497 | .fade-view.ng-enter { 498 | transition: 1.0s linear all; 499 | opacity: 0; 500 | } 501 | 502 | 503 | /* The finishing CSS styles for the enter animation for loading views*/ 504 | 505 | .fade-view.ng-enter.ng-enter-active { 506 | opacity: 1; 507 | } 508 | 509 | 510 | /* The starting CSS styles for the enter animation for loading ng-repeat*/ 511 | 512 | .fade-repeat.ng-enter { 513 | transition: 0.5s linear all; 514 | opacity: 0; 515 | } 516 | 517 | 518 | /* The finishing CSS styles for the enter animation for loading ng-repeat*/ 519 | 520 | .fade-repeat.ng-enter.ng-enter-active { 521 | opacity: 1; 522 | } 523 | 524 | progress { 525 | background-color: #f3f3f3; 526 | height: 1em; 527 | } 528 | 529 | .text-in-bar { 530 | position: relative; 531 | top: -1.4em; 532 | } 533 | 534 | .transferRate { 535 | position: absolute; 536 | left: -.9em; 537 | }.testing-class { 538 | font-size: 4em; 539 | } 540 | 541 | .modal-content { 542 | background-color: rgba(0, 0, 0, 0.6); 543 | } 544 | 545 | img { 546 | display: block; 547 | margin: auto; 548 | border: 1px; 549 | padding: 10px; 550 | } 551 | 552 | 553 | .mkstream-logo { 554 | height: 80px; 555 | width: 300px; 556 | } 557 | 558 | .mk-upload, .mk-download { 559 | margin-bottom: 5px; 560 | } 561 | 562 | -------------------------------------------------------------------------------- /client/lib/nochunkbufferfixpeer.min.js: -------------------------------------------------------------------------------- 1 | !function e(t,n,i){function r(s,a){if(!n[s]){if(!t[s]){var c="function"==typeof require&&require;if(!a&&c)return c(s,!0);if(o)return o(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var p=n[s]={exports:{}};t[s][0].call(p.exports,function(e){var n=t[s][1][e];return r(n?n:e)},p,p.exports,e,t,n,i)}return n[s].exports}for(var o="function"==typeof require&&require,s=0;sr.chunkedMTU)return void this._sendChunks(i);r.supports.sctp?r.supports.binaryBlob?this._bufferedSend(i):r.blobToArrayBuffer(i,function(e){n._bufferedSend(e)}):r.blobToBinaryString(i,function(e){n._bufferedSend(e)})}else this._bufferedSend(e)},i.prototype._bufferedSend=function(e){(this._buffering||!this._trySend(e))&&(this._buffer.push(e),this.bufferSize=this._buffer.length)},i.prototype._trySend=function(e){function t(){return n._buffering=!0,setTimeout(function(){n._buffering=!1,n._tryBuffer()},100),!1}var n=this;if(n._dc.bufferedAmount>15728640)return t();try{this._dc.send(e)}catch(i){return t()}return!0},i.prototype._tryBuffer=function(){if(0!==this._buffer.length){var e=this._buffer[0];this._trySend(e)&&(this._buffer.shift(),this.bufferSize=this._buffer.length,this._tryBuffer())}},i.prototype._sendChunks=function(e){for(var t=r.chunk(e),n=0,i=t.length;i>n;n+=1){var e=t[n];this.send(e,!0)}},i.prototype.handleMessage=function(e){var t=e.payload;switch(e.type){case"ANSWER":this._peerBrowser=t.browser,s.handleSDP(e.type,this,t.sdp);break;case"CANDIDATE":s.handleCandidate(this,t.candidate);break;default:r.warn("Unrecognized message type:",e.type,"from peer:",this.peer)}},t.exports=i},{"./negotiator":5,"./util":8,eventemitter3:9,reliable:12}],3:[function(e,t,n){window.Socket=e("./socket"),window.MediaConnection=e("./mediaconnection"),window.DataConnection=e("./dataconnection"),window.Peer=e("./peer"),window.RTCPeerConnection=e("./adapter").RTCPeerConnection,window.RTCSessionDescription=e("./adapter").RTCSessionDescription,window.RTCIceCandidate=e("./adapter").RTCIceCandidate,window.Negotiator=e("./negotiator"),window.util=e("./util"),window.BinaryPack=e("js-binarypack")},{"./adapter":1,"./dataconnection":2,"./mediaconnection":4,"./negotiator":5,"./peer":6,"./socket":7,"./util":8,"js-binarypack":10}],4:[function(e,t,n){function i(e,t,n){return this instanceof i?(o.call(this),this.options=r.extend({},n),this.open=!1,this.type="media",this.peer=e,this.provider=t,this.metadata=this.options.metadata,this.localStream=this.options._stream,this.id=this.options.connectionId||i._idPrefix+r.randomToken(),void(this.localStream&&s.startConnection(this,{_stream:this.localStream,originator:!0}))):new i(e,t,n)}var r=e("./util"),o=e("eventemitter3"),s=e("./negotiator");r.inherits(i,o),i._idPrefix="mc_",i.prototype.addStream=function(e){r.log("Receiving stream",e),this.remoteStream=e,this.emit("stream",e)},i.prototype.handleMessage=function(e){var t=e.payload;switch(e.type){case"ANSWER":s.handleSDP(e.type,this,t.sdp),this.open=!0;break;case"CANDIDATE":s.handleCandidate(this,t.candidate);break;default:r.warn("Unrecognized message type:",e.type,"from peer:",this.peer)}},i.prototype.answer=function(e){if(this.localStream)return void r.warn("Local stream already exists on this MediaConnection. Are you answering a call twice?");this.options._payload._stream=e,this.localStream=e,s.startConnection(this,this.options._payload);for(var t=this.provider._getMessages(this.id),n=0,i=t.length;i>n;n+=1)this.handleMessage(t[n]);this.open=!0},i.prototype.close=function(){this.open&&(this.open=!1,s.cleanup(this),this.emit("close"))},t.exports=i},{"./negotiator":5,"./util":8,eventemitter3:9}],5:[function(e,t,n){var i=e("./util"),r=e("./adapter").RTCPeerConnection,o=e("./adapter").RTCSessionDescription,s=e("./adapter").RTCIceCandidate,a={pcs:{data:{},media:{}},queue:[]};a._idPrefix="pc_",a.startConnection=function(e,t){var n=a._getPeerConnection(e,t);if("media"===e.type&&t._stream&&n.addStream(t._stream),e.pc=e.peerConnection=n,t.originator){if("data"===e.type){var r={};i.supports.sctp||(r={reliable:t.reliable});var o=n.createDataChannel(e.label,r);e.initialize(o)}i.supports.onnegotiationneeded||a._makeOffer(e)}else a.handleSDP("OFFER",e,t.sdp)},a._getPeerConnection=function(e,t){a.pcs[e.type]||i.error(e.type+" is not a valid connection type. Maybe you overrode the `type` property somewhere."),a.pcs[e.type][e.peer]||(a.pcs[e.type][e.peer]={});var n;return a.pcs[e.type][e.peer],t.pc&&(n=a.pcs[e.type][e.peer][t.pc]),n&&"stable"===n.signalingState||(n=a._startPeerConnection(e)),n},a._startPeerConnection=function(e){i.log("Creating RTCPeerConnection.");var t=a._idPrefix+i.randomToken(),n={};"data"!==e.type||i.supports.sctp?"media"===e.type&&(n={optional:[{DtlsSrtpKeyAgreement:!0}]}):n={optional:[{RtpDataChannels:!0}]};var o=new r(e.provider.options.config,n);return a.pcs[e.type][e.peer][t]=o,a._setupListeners(e,o,t),o},a._setupListeners=function(e,t,n){var r=e.peer,o=e.id,s=e.provider;i.log("Listening for ICE candidates."),t.onicecandidate=function(t){t.candidate&&(i.log("Received ICE candidates for:",e.peer),s.socket.send({type:"CANDIDATE",payload:{candidate:t.candidate,type:e.type,connectionId:e.id},dst:r}))},t.oniceconnectionstatechange=function(){switch(t.iceConnectionState){case"disconnected":case"failed":i.log("iceConnectionState is disconnected, closing connections to "+r),e.close();break;case"completed":t.onicecandidate=i.noop}},t.onicechange=t.oniceconnectionstatechange,i.log("Listening for `negotiationneeded`"),t.onnegotiationneeded=function(){i.log("`negotiationneeded` triggered"),"stable"==t.signalingState?a._makeOffer(e):i.log("onnegotiationneeded triggered when not stable. Is another connection being established?")},i.log("Listening for data channel"),t.ondatachannel=function(e){i.log("Received data channel");var t=e.channel,n=s.getConnection(r,o);n.initialize(t)},i.log("Listening for remote stream"),t.onaddstream=function(e){i.log("Received remote stream");var t=e.stream,n=s.getConnection(r,o);"media"===n.type&&n.addStream(t)}},a.cleanup=function(e){i.log("Cleaning up PeerConnection to "+e.peer);var t=e.pc;!t||"closed"===t.readyState&&"closed"===t.signalingState||(t.close(),e.pc=null)},a._makeOffer=function(e){var t=e.pc;t.createOffer(function(n){i.log("Created offer."),!i.supports.sctp&&"data"===e.type&&e.reliable&&(n.sdp=Reliable.higherBandwidthSDP(n.sdp)),t.setLocalDescription(n,function(){i.log("Set localDescription: offer","for:",e.peer),e.provider.socket.send({type:"OFFER",payload:{sdp:n,type:e.type,label:e.label,connectionId:e.id,reliable:e.reliable,serialization:e.serialization,metadata:e.metadata,browser:i.browser},dst:e.peer})},function(t){e.provider.emitError("webrtc",t),i.log("Failed to setLocalDescription, ",t)})},function(t){e.provider.emitError("webrtc",t),i.log("Failed to createOffer, ",t)},e.options.constraints)},a._makeAnswer=function(e){var t=e.pc;t.createAnswer(function(n){i.log("Created answer."),!i.supports.sctp&&"data"===e.type&&e.reliable&&(n.sdp=Reliable.higherBandwidthSDP(n.sdp)),t.setLocalDescription(n,function(){i.log("Set localDescription: answer","for:",e.peer),e.provider.socket.send({type:"ANSWER",payload:{sdp:n,type:e.type,connectionId:e.id,browser:i.browser},dst:e.peer})},function(t){e.provider.emitError("webrtc",t),i.log("Failed to setLocalDescription, ",t)})},function(t){e.provider.emitError("webrtc",t),i.log("Failed to create answer, ",t)})},a.handleSDP=function(e,t,n){n=new o(n);var r=t.pc;i.log("Setting remote description",n),r.setRemoteDescription(n,function(){i.log("Set remoteDescription:",e,"for:",t.peer),"OFFER"===e&&a._makeAnswer(t)},function(e){t.provider.emitError("webrtc",e),i.log("Failed to setRemoteDescription, ",e)})},a.handleCandidate=function(e,t){var n=t.candidate,r=t.sdpMLineIndex;e.pc.addIceCandidate(new s({sdpMLineIndex:r,candidate:n})),i.log("Added ICE candidate for:",e.peer)},t.exports=a},{"./adapter":1,"./util":8}],6:[function(e,t,n){function i(e,t){return this instanceof i?(o.call(this),e&&e.constructor==Object?(t=e,e=void 0):e&&(e=e.toString()),t=r.extend({debug:0,host:r.CLOUD_HOST,port:r.CLOUD_PORT,key:"peerjs",path:"/",token:r.randomToken(),config:r.defaultConfig},t),this.options=t,"/"===t.host&&(t.host=window.location.hostname),"/"!==t.path[0]&&(t.path="/"+t.path),"/"!==t.path[t.path.length-1]&&(t.path+="/"),void 0===t.secure&&t.host!==r.CLOUD_HOST&&(t.secure=r.isSecure()),t.logFunction&&r.setLogFunction(t.logFunction),r.setLogLevel(t.debug),r.supports.audioVideo||r.supports.data?r.validateId(e)?r.validateKey(t.key)?t.secure&&"0.peerjs.com"===t.host?void this._delayedAbort("ssl-unavailable","The cloud server currently does not support HTTPS. Please run your own PeerServer to use HTTPS."):(this.destroyed=!1,this.disconnected=!1,this.open=!1,this.connections={},this._lostMessages={},this._initializeServerConnection(),void(e?this._initialize(e):this._retrieveId())):void this._delayedAbort("invalid-key",'API KEY "'+t.key+'" is invalid'):void this._delayedAbort("invalid-id",'ID "'+e+'" is invalid'):void this._delayedAbort("browser-incompatible","The current browser does not support WebRTC")):new i(e,t)}var r=e("./util"),o=e("eventemitter3"),s=e("./socket"),a=e("./mediaconnection"),c=e("./dataconnection");r.inherits(i,o),i.prototype._initializeServerConnection=function(){var e=this;this.socket=new s(this.options.secure,this.options.host,this.options.port,this.options.path,this.options.key),this.socket.on("message",function(t){e._handleMessage(t)}),this.socket.on("error",function(t){e._abort("socket-error",t)}),this.socket.on("disconnected",function(){e.disconnected||(e.emitError("network","Lost connection to server."),e.disconnect())}),this.socket.on("close",function(){e.disconnected||e._abort("socket-closed","Underlying socket is already closed.")})},i.prototype._retrieveId=function(e){var t=this,n=new XMLHttpRequest,i=this.options.secure?"https://":"http://",o=i+this.options.host+":"+this.options.port+this.options.path+this.options.key+"/id",s="?ts="+(new Date).getTime()+Math.random();o+=s,n.open("get",o,!0),n.onerror=function(e){r.error("Error retrieving ID",e);var n="";"/"===t.options.path&&t.options.host!==r.CLOUD_HOST&&(n=" If you passed in a `path` to your self-hosted PeerServer, you'll also need to pass in that same path when creating a new Peer."),t._abort("server-error","Could not get an ID from the server."+n)},n.onreadystatechange=function(){return 4===n.readyState?200!==n.status?void n.onerror():void t._initialize(n.responseText):void 0},n.send(null)},i.prototype._initialize=function(e){this.id=e,this.socket.start(this.id,this.options.token)},i.prototype._handleMessage=function(e){var t,n=e.type,i=e.payload,o=e.src;switch(n){case"OPEN":this.emit("open",this.id),this.open=!0;break;case"ERROR":this._abort("server-error",i.msg);break;case"ID-TAKEN":this._abort("unavailable-id","ID `"+this.id+"` is taken");break;case"INVALID-KEY":this._abort("invalid-key",'API KEY "'+this.options.key+'" is invalid');break;case"LEAVE":r.log("Received leave message from",o),this._cleanupPeer(o);break;case"EXPIRE":this.emitError("peer-unavailable","Could not connect to peer "+o);break;case"OFFER":var s=i.connectionId;if(t=this.getConnection(o,s))r.warn("Offer received for existing Connection ID:",s);else{if("media"===i.type)t=new a(o,this,{connectionId:s,_payload:i,metadata:i.metadata}),this._addConnection(o,t),this.emit("call",t);else{if("data"!==i.type)return void r.warn("Received malformed connection type:",i.type);t=new c(o,this,{connectionId:s,_payload:i,metadata:i.metadata,label:i.label,serialization:i.serialization,reliable:i.reliable}),this._addConnection(o,t),this.emit("connection",t)}for(var u=this._getMessages(s),p=0,h=u.length;h>p;p+=1)t.handleMessage(u[p])}break;default:if(!i)return void r.warn("You received a malformed message from "+o+" of type "+n);var d=i.connectionId;t=this.getConnection(o,d),t&&t.pc?t.handleMessage(e):d?this._storeMessage(d,e):r.warn("You received an unrecognized message:",e)}},i.prototype._storeMessage=function(e,t){this._lostMessages[e]||(this._lostMessages[e]=[]),this._lostMessages[e].push(t)},i.prototype._getMessages=function(e){var t=this._lostMessages[e];return t?(delete this._lostMessages[e],t):[]},i.prototype.connect=function(e,t){if(this.disconnected)return r.warn("You cannot connect to a new Peer because you called .disconnect() on this Peer and ended your connection with the server. You can create a new Peer to reconnect, or call reconnect on this peer if you believe its ID to still be available."),void this.emitError("disconnected","Cannot connect to new Peer after disconnecting from server.");var n=new c(e,this,t);return this._addConnection(e,n),n},i.prototype.call=function(e,t,n){if(this.disconnected)return r.warn("You cannot connect to a new Peer because you called .disconnect() on this Peer and ended your connection with the server. You can create a new Peer to reconnect."),void this.emitError("disconnected","Cannot connect to new Peer after disconnecting from server.");if(!t)return void r.error("To call a peer, you must provide a stream from your browser's `getUserMedia`.");n=n||{},n._stream=t;var i=new a(e,this,n);return this._addConnection(e,i),i},i.prototype._addConnection=function(e,t){this.connections[e]||(this.connections[e]=[]),this.connections[e].push(t)},i.prototype.getConnection=function(e,t){var n=this.connections[e];if(!n)return null;for(var i=0,r=n.length;r>i;i++)if(n[i].id===t)return n[i];return null},i.prototype._delayedAbort=function(e,t){var n=this;r.setZeroTimeout(function(){n._abort(e,t)})},i.prototype._abort=function(e,t){r.error("Aborting!"),this._lastServerId?this.disconnect():this.destroy(),this.emitError(e,t)},i.prototype.emitError=function(e,t){r.error("Error:",t),"string"==typeof t&&(t=new Error(t)),t.type=e,this.emit("error",t)},i.prototype.destroy=function(){this.destroyed||(this._cleanup(),this.disconnect(),this.destroyed=!0)},i.prototype._cleanup=function(){if(this.connections)for(var e=Object.keys(this.connections),t=0,n=e.length;n>t;t++)this._cleanupPeer(e[t]);this.emit("close")},i.prototype._cleanupPeer=function(e){for(var t=this.connections[e],n=0,i=t.length;i>n;n+=1)t[n].close()},i.prototype.disconnect=function(){var e=this;r.setZeroTimeout(function(){e.disconnected||(e.disconnected=!0,e.open=!1,e.socket&&e.socket.close(),e.emit("disconnected",e.id),e._lastServerId=e.id,e.id=null)})},i.prototype.reconnect=function(){if(this.disconnected&&!this.destroyed)r.log("Attempting reconnection to server with ID "+this._lastServerId),this.disconnected=!1,this._initializeServerConnection(),this._initialize(this._lastServerId);else{if(this.destroyed)throw new Error("This peer cannot reconnect to the server. It has already been destroyed.");if(this.disconnected||this.open)throw new Error("Peer "+this.id+" cannot reconnect because it is not disconnected from the server!");r.error("In a hurry? We're still trying to make the initial connection!")}},i.prototype.listAllPeers=function(e){e=e||function(){};var t=this,n=new XMLHttpRequest,i=this.options.secure?"https://":"http://",o=i+this.options.host+":"+this.options.port+this.options.path+this.options.key+"/peers",s="?ts="+(new Date).getTime()+Math.random();o+=s,n.open("get",o,!0),n.onerror=function(n){t._abort("server-error","Could not get peers from the server."),e([])},n.onreadystatechange=function(){if(4===n.readyState){if(401===n.status){var i="";throw i=t.options.host!==r.CLOUD_HOST?"It looks like you're using the cloud server. You can email team@peerjs.com to enable peer listing for your API key.":"You need to enable `allow_discovery` on your self-hosted PeerServer to use this feature.",e([]),new Error("It doesn't look like you have permission to list peers IDs. "+i)}e(200!==n.status?[]:JSON.parse(n.responseText))}},n.send(null)},t.exports=i},{"./dataconnection":2,"./mediaconnection":4,"./socket":7,"./util":8,eventemitter3:9}],7:[function(e,t,n){function i(e,t,n,r,s){if(!(this instanceof i))return new i(e,t,n,r,s);o.call(this),this.disconnected=!1,this._queue=[];var a=e?"https://":"http://",c=e?"wss://":"ws://";this._httpUrl=a+t+":"+n+r+s,this._wsUrl=c+t+":"+n+r+"peerjs?key="+s}var r=e("./util"),o=e("eventemitter3");r.inherits(i,o),i.prototype.start=function(e,t){this.id=e,this._httpUrl+="/"+e+"/"+t,this._wsUrl+="&id="+e+"&token="+t,this._startXhrStream(),this._startWebSocket()},i.prototype._startWebSocket=function(e){var t=this;this._socket||(this._socket=new WebSocket(this._wsUrl),this._socket.onmessage=function(e){try{var n=JSON.parse(e.data)}catch(i){return void r.log("Invalid server message",e.data)}t.emit("message",n)},this._socket.onclose=function(e){r.log("Socket closed."),t.disconnected=!0,t.emit("disconnected")},this._socket.onopen=function(){t._timeout&&(clearTimeout(t._timeout),setTimeout(function(){t._http.abort(),t._http=null},5e3)),t._sendQueuedMessages(),r.log("Socket open")})},i.prototype._startXhrStream=function(e){try{var t=this;this._http=new XMLHttpRequest,this._http._index=1,this._http._streamIndex=e||0,this._http.open("post",this._httpUrl+"/id?i="+this._http._streamIndex,!0),this._http.onerror=function(){clearTimeout(t._timeout),t.emit("disconnected")},this._http.onreadystatechange=function(){2==this.readyState&&this.old?(this.old.abort(),delete this.old):this.readyState>2&&200===this.status&&this.responseText&&t._handleStream(this)},this._http.send(null),this._setHTTPTimeout()}catch(n){r.log("XMLHttpRequest not available; defaulting to WebSockets")}},i.prototype._handleStream=function(e){var t=e.responseText.split("\n");if(e._buffer)for(;e._buffer.length>0;){var n=e._buffer.shift(),i=t[n];try{i=JSON.parse(i)}catch(o){e._buffer.shift(n);break}this.emit("message",i)}var s=t[e._index];if(s)if(e._index+=1,e._index===t.length)e._buffer||(e._buffer=[]),e._buffer.push(e._index-1);else{try{s=JSON.parse(s)}catch(o){return void r.log("Invalid server message",s)}this.emit("message",s)}},i.prototype._setHTTPTimeout=function(){var e=this;this._timeout=setTimeout(function(){var t=e._http;e._wsOpen()?t.abort():(e._startXhrStream(t._streamIndex+1),e._http.old=t)},25e3)},i.prototype._wsOpen=function(){return this._socket&&1==this._socket.readyState},i.prototype._sendQueuedMessages=function(){for(var e=0,t=this._queue.length;t>e;e+=1)this.send(this._queue[e])},i.prototype.send=function(e){if(!this.disconnected){if(!this.id)return void this._queue.push(e);if(!e.type)return void this.emit("error","Invalid message");var t=JSON.stringify(e);if(this._wsOpen())this._socket.send(t);else{var n=new XMLHttpRequest,i=this._httpUrl+"/"+e.type.toLowerCase();n.open("post",i,!0),n.setRequestHeader("Content-Type","application/json"),n.send(t)}}},i.prototype.close=function(){!this.disconnected&&this._wsOpen()&&(this._socket.close(),this.disconnected=!0)},t.exports=i},{"./util":8,eventemitter3:9}],8:[function(e,t,n){var i={iceServers:[{url:"stun:stun.l.google.com:19302"}]},r=1,o=e("js-binarypack"),s=e("./adapter").RTCPeerConnection,a={noop:function(){},CLOUD_HOST:"0.peerjs.com",CLOUD_PORT:9e3,chunkedBrowsers:{Chrome:1},chunkedMTU:16300,logLevel:0,setLogLevel:function(e){var t=parseInt(e,10);isNaN(parseInt(e,10))?a.logLevel=e?3:0:a.logLevel=t,a.log=a.warn=a.error=a.noop,a.logLevel>0&&(a.error=a._printWith("ERROR")),a.logLevel>1&&(a.warn=a._printWith("WARNING")),a.logLevel>2&&(a.log=a._print)},setLogFunction:function(e){e.constructor!==Function?a.warn("The log function you passed in is not a function. Defaulting to regular logs."):a._print=e},_printWith:function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e),a._print.apply(a,t)}},_print:function(){var e=!1,t=Array.prototype.slice.call(arguments);t.unshift("PeerJS: ");for(var n=0,i=t.length;i>n;n++)t[n]instanceof Error&&(t[n]="("+t[n].name+") "+t[n].message,e=!0);e?console.error.apply(console,t):console.log.apply(console,t)},defaultConfig:i,browser:function(){return window.mozRTCPeerConnection?"Firefox":window.webkitRTCPeerConnection?"Chrome":window.RTCPeerConnection?"Supported":"Unsupported"}(),supports:function(){if("undefined"==typeof s)return{};var e,t,n=!0,r=!0,o=!1,c=!1,u=!!window.webkitRTCPeerConnection;try{e=new s(i,{optional:[{RtpDataChannels:!0}]})}catch(p){n=!1,r=!1}if(n)try{t=e.createDataChannel("_PEERJSTEST")}catch(p){n=!1}if(n){try{t.binaryType="blob",o=!0}catch(p){}var h=new s(i,{});try{var d=h.createDataChannel("_PEERJSRELIABLETEST",{});c=d.reliable}catch(p){}h.close()}if(r&&(r=!!e.addStream),!u&&n){var l=new s(i,{optional:[{RtpDataChannels:!0}]});l.onnegotiationneeded=function(){u=!0,a&&a.supports&&(a.supports.onnegotiationneeded=!0)},l.createDataChannel("_PEERJSNEGOTIATIONTEST"),setTimeout(function(){l.close()},1e3)}return e&&e.close(),{audioVideo:r,data:n,binaryBlob:o,binary:c,reliable:c,sctp:c,onnegotiationneeded:u}}(),validateId:function(e){return!e||/^[A-Za-z0-9_-]+(?:[ _-][A-Za-z0-9]+)*$/.exec(e)},validateKey:function(e){return!e||/^[A-Za-z0-9_-]+(?:[ _-][A-Za-z0-9]+)*$/.exec(e)},debug:!1,inherits:function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})},extend:function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e},pack:o.pack,unpack:o.unpack,log:function(){if(a.debug){var e=!1,t=Array.prototype.slice.call(arguments);t.unshift("PeerJS: ");for(var n=0,i=t.length;i>n;n++)t[n]instanceof Error&&(t[n]="("+t[n].name+") "+t[n].message,e=!0);e?console.error.apply(console,t):console.log.apply(console,t)}},setZeroTimeout:function(e){function t(t){i.push(t),e.postMessage(r,"*")}function n(t){t.source==e&&t.data==r&&(t.stopPropagation&&t.stopPropagation(),i.length&&i.shift()())}var i=[],r="zero-timeout-message";return e.addEventListener?e.addEventListener("message",n,!0):e.attachEvent&&e.attachEvent("onmessage",n),t}(window),chunk:function(e){for(var t=[],n=e.size,i=index=0,o=Math.ceil(n/a.chunkedMTU);n>i;){var s=Math.min(n,i+a.chunkedMTU),c=e.slice(i,s),u={__peerData:r,n:index,data:c,total:o};t.push(u),i=s,index+=1}return r+=1,t},blobToArrayBuffer:function(e,t){var n=new FileReader;n.onload=function(e){t(e.target.result)},n.readAsArrayBuffer(e)},blobToBinaryString:function(e,t){var n=new FileReader;n.onload=function(e){t(e.target.result)},n.readAsBinaryString(e)},binaryStringToArrayBuffer:function(e){for(var t=new Uint8Array(e.length),n=0;nt;t++)i.push(this._events[e][t].fn);return i},r.prototype.emit=function(e,t,n,i,r,o){if(!this._events||!this._events[e])return!1;var s,a,c,u=this._events[e],p=u.length,h=arguments.length,d=u[0];if(1===p){switch(d.once&&this.removeListener(e,d.fn,!0),h){case 1:return d.fn.call(d.context),!0;case 2:return d.fn.call(d.context,t),!0;case 3:return d.fn.call(d.context,t,n),!0;case 4:return d.fn.call(d.context,t,n,i),!0;case 5:return d.fn.call(d.context,t,n,i,r),!0;case 6:return d.fn.call(d.context,t,n,i,r,o),!0}for(a=1,s=new Array(h-1);h>a;a++)s[a-1]=arguments[a];d.fn.apply(d.context,s)}else for(a=0;p>a;a++)switch(u[a].once&&this.removeListener(e,u[a].fn,!0),h){case 1:u[a].fn.call(u[a].context);break;case 2:u[a].fn.call(u[a].context,t);break;case 3:u[a].fn.call(u[a].context,t,n);break;default:if(!s)for(c=1,s=new Array(h-1);h>c;c++)s[c-1]=arguments[c];u[a].fn.apply(u[a].context,s)}return!0},r.prototype.on=function(e,t,n){return this._events||(this._events={}),this._events[e]||(this._events[e]=[]),this._events[e].push(new i(t,n||this)),this},r.prototype.once=function(e,t,n){return this._events||(this._events={}),this._events[e]||(this._events[e]=[]),this._events[e].push(new i(t,n||this,!0)),this},r.prototype.removeListener=function(e,t,n){if(!this._events||!this._events[e])return this;var i=this._events[e],r=[];if(t)for(var o=0,s=i.length;s>o;o++)i[o].fn!==t&&i[o].once!==n&&r.push(i[o]);return r.length?this._events[e]=r:this._events[e]=null,this},r.prototype.removeAllListeners=function(e){return this._events?(e?this._events[e]=null:this._events={},this):this},r.prototype.off=r.prototype.removeListener,r.prototype.addListener=r.prototype.on,r.prototype.setMaxListeners=function(){return this},r.EventEmitter=r,r.EventEmitter2=r,r.EventEmitter3=r,"object"==typeof t&&t.exports&&(t.exports=r)},{}],10:[function(e,t,n){function i(e){this.index=0,this.dataBuffer=e,this.dataView=new Uint8Array(this.dataBuffer),this.length=this.dataBuffer.byteLength}function r(){this.bufferBuilder=new a}function o(e){var t=e.charCodeAt(0);return 2047>=t?"00":65535>=t?"000":2097151>=t?"0000":67108863>=t?"00000":"000000"}function s(e){return e.length>600?new Blob([e]).size:e.replace(/[^\u0000-\u007F]/g,o).length}var a=e("./bufferbuilder").BufferBuilder,c=e("./bufferbuilder").binaryFeatures,u={unpack:function(e){var t=new i(e);return t.unpack()},pack:function(e){var t=new r;t.pack(e);var n=t.getBuffer();return n}};t.exports=u,i.prototype.unpack=function(){var e=this.unpack_uint8();if(128>e){var t=e;return t}if(32>(224^e)){var n=(224^e)-32;return n}var i;if((i=160^e)<=15)return this.unpack_raw(i);if((i=176^e)<=15)return this.unpack_string(i);if((i=144^e)<=15)return this.unpack_array(i);if((i=128^e)<=15)return this.unpack_map(i);switch(e){case 192:return null;case 193:return;case 194:return!1;case 195:return!0;case 202:return this.unpack_float();case 203:return this.unpack_double();case 204:return this.unpack_uint8();case 205:return this.unpack_uint16();case 206:return this.unpack_uint32();case 207:return this.unpack_uint64();case 208:return this.unpack_int8();case 209:return this.unpack_int16();case 210:return this.unpack_int32();case 211:return this.unpack_int64();case 212:return;case 213:return;case 214:return;case 215:return;case 216:return i=this.unpack_uint16(),this.unpack_string(i);case 217:return i=this.unpack_uint32(),this.unpack_string(i);case 218:return i=this.unpack_uint16(),this.unpack_raw(i);case 219:return i=this.unpack_uint32(),this.unpack_raw(i);case 220:return i=this.unpack_uint16(),this.unpack_array(i);case 221:return i=this.unpack_uint32(),this.unpack_array(i);case 222:return i=this.unpack_uint16(),this.unpack_map(i);case 223:return i=this.unpack_uint32(),this.unpack_map(i)}},i.prototype.unpack_uint8=function(){var e=255&this.dataView[this.index];return this.index++,e},i.prototype.unpack_uint16=function(){var e=this.read(2),t=256*(255&e[0])+(255&e[1]);return this.index+=2,t},i.prototype.unpack_uint32=function(){var e=this.read(4),t=256*(256*(256*e[0]+e[1])+e[2])+e[3];return this.index+=4,t},i.prototype.unpack_uint64=function(){var e=this.read(8),t=256*(256*(256*(256*(256*(256*(256*e[0]+e[1])+e[2])+e[3])+e[4])+e[5])+e[6])+e[7];return this.index+=8,t},i.prototype.unpack_int8=function(){var e=this.unpack_uint8();return 128>e?e:e-256},i.prototype.unpack_int16=function(){var e=this.unpack_uint16();return 32768>e?e:e-65536},i.prototype.unpack_int32=function(){var e=this.unpack_uint32();return er;)t=i[r],128>t?(o+=String.fromCharCode(t),r++):32>(192^t)?(n=(192^t)<<6|63&i[r+1],o+=String.fromCharCode(n),r+=2):(n=(15&t)<<12|(63&i[r+1])<<6|63&i[r+2],o+=String.fromCharCode(n),r+=3);return this.index+=e,o},i.prototype.unpack_array=function(e){for(var t=new Array(e),n=0;e>n;n++)t[n]=this.unpack();return t},i.prototype.unpack_map=function(e){for(var t={},n=0;e>n;n++){var i=this.unpack(),r=this.unpack();t[i]=r}return t},i.prototype.unpack_float=function(){var e=this.unpack_uint32(),t=e>>31,n=(e>>23&255)-127,i=8388607&e|8388608;return(0==t?1:-1)*i*Math.pow(2,n-23)},i.prototype.unpack_double=function(){var e=this.unpack_uint32(),t=this.unpack_uint32(),n=e>>31,i=(e>>20&2047)-1023,r=1048575&e|1048576,o=r*Math.pow(2,i-20)+t*Math.pow(2,i-52);return(0==n?1:-1)*o},i.prototype.read=function(e){var t=this.index;if(t+e<=this.length)return this.dataView.subarray(t,t+e);throw new Error("BinaryPackFailure: read index out of range")},r.prototype.getBuffer=function(){return this.bufferBuilder.getBuffer()},r.prototype.pack=function(e){var t=typeof e;if("string"==t)this.pack_string(e);else if("number"==t)Math.floor(e)===e?this.pack_integer(e):this.pack_double(e);else if("boolean"==t)e===!0?this.bufferBuilder.append(195):e===!1&&this.bufferBuilder.append(194);else if("undefined"==t)this.bufferBuilder.append(192);else{if("object"!=t)throw new Error('Type "'+t+'" not yet supported');if(null===e)this.bufferBuilder.append(192);else{var n=e.constructor;if(n==Array)this.pack_array(e);else if(n==Blob||n==File)this.pack_bin(e);else if(n==ArrayBuffer)c.useArrayBufferView?this.pack_bin(new Uint8Array(e)):this.pack_bin(e);else if("BYTES_PER_ELEMENT"in e)c.useArrayBufferView?this.pack_bin(new Uint8Array(e.buffer)):this.pack_bin(e.buffer);else if(n==Object)this.pack_object(e);else if(n==Date)this.pack_string(e.toString());else{ 2 | if("function"!=typeof e.toBinaryPack)throw new Error('Type "'+n.toString()+'" not yet supported');this.bufferBuilder.append(e.toBinaryPack())}}}this.bufferBuilder.flush()},r.prototype.pack_bin=function(e){var t=e.length||e.byteLength||e.size;if(15>=t)this.pack_uint8(160+t);else if(65535>=t)this.bufferBuilder.append(218),this.pack_uint16(t);else{if(!(4294967295>=t))throw new Error("Invalid length");this.bufferBuilder.append(219),this.pack_uint32(t)}this.bufferBuilder.append(e)},r.prototype.pack_string=function(e){var t=s(e);if(15>=t)this.pack_uint8(176+t);else if(65535>=t)this.bufferBuilder.append(216),this.pack_uint16(t);else{if(!(4294967295>=t))throw new Error("Invalid length");this.bufferBuilder.append(217),this.pack_uint32(t)}this.bufferBuilder.append(e)},r.prototype.pack_array=function(e){var t=e.length;if(15>=t)this.pack_uint8(144+t);else if(65535>=t)this.bufferBuilder.append(220),this.pack_uint16(t);else{if(!(4294967295>=t))throw new Error("Invalid length");this.bufferBuilder.append(221),this.pack_uint32(t)}for(var n=0;t>n;n++)this.pack(e[n])},r.prototype.pack_integer=function(e){if(e>=-32&&127>=e)this.bufferBuilder.append(255&e);else if(e>=0&&255>=e)this.bufferBuilder.append(204),this.pack_uint8(e);else if(e>=-128&&127>=e)this.bufferBuilder.append(208),this.pack_int8(e);else if(e>=0&&65535>=e)this.bufferBuilder.append(205),this.pack_uint16(e);else if(e>=-32768&&32767>=e)this.bufferBuilder.append(209),this.pack_int16(e);else if(e>=0&&4294967295>=e)this.bufferBuilder.append(206),this.pack_uint32(e);else if(e>=-2147483648&&2147483647>=e)this.bufferBuilder.append(210),this.pack_int32(e);else if(e>=-0x8000000000000000&&0x8000000000000000>=e)this.bufferBuilder.append(211),this.pack_int64(e);else{if(!(e>=0&&0x10000000000000000>=e))throw new Error("Invalid integer");this.bufferBuilder.append(207),this.pack_uint64(e)}},r.prototype.pack_double=function(e){var t=0;0>e&&(t=1,e=-e);var n=Math.floor(Math.log(e)/Math.LN2),i=e/Math.pow(2,n)-1,r=Math.floor(i*Math.pow(2,52)),o=Math.pow(2,32),s=t<<31|n+1023<<20|r/o&1048575,a=r%o;this.bufferBuilder.append(203),this.pack_int32(s),this.pack_int32(a)},r.prototype.pack_object=function(e){var t=Object.keys(e),n=t.length;if(15>=n)this.pack_uint8(128+n);else if(65535>=n)this.bufferBuilder.append(222),this.pack_uint16(n);else{if(!(4294967295>=n))throw new Error("Invalid length");this.bufferBuilder.append(223),this.pack_uint32(n)}for(var i in e)e.hasOwnProperty(i)&&(this.pack(i),this.pack(e[i]))},r.prototype.pack_uint8=function(e){this.bufferBuilder.append(e)},r.prototype.pack_uint16=function(e){this.bufferBuilder.append(e>>8),this.bufferBuilder.append(255&e)},r.prototype.pack_uint32=function(e){var t=4294967295&e;this.bufferBuilder.append((4278190080&t)>>>24),this.bufferBuilder.append((16711680&t)>>>16),this.bufferBuilder.append((65280&t)>>>8),this.bufferBuilder.append(255&t)},r.prototype.pack_uint64=function(e){var t=e/Math.pow(2,32),n=e%Math.pow(2,32);this.bufferBuilder.append((4278190080&t)>>>24),this.bufferBuilder.append((16711680&t)>>>16),this.bufferBuilder.append((65280&t)>>>8),this.bufferBuilder.append(255&t),this.bufferBuilder.append((4278190080&n)>>>24),this.bufferBuilder.append((16711680&n)>>>16),this.bufferBuilder.append((65280&n)>>>8),this.bufferBuilder.append(255&n)},r.prototype.pack_int8=function(e){this.bufferBuilder.append(255&e)},r.prototype.pack_int16=function(e){this.bufferBuilder.append((65280&e)>>8),this.bufferBuilder.append(255&e)},r.prototype.pack_int32=function(e){this.bufferBuilder.append(e>>>24&255),this.bufferBuilder.append((16711680&e)>>>16),this.bufferBuilder.append((65280&e)>>>8),this.bufferBuilder.append(255&e)},r.prototype.pack_int64=function(e){var t=Math.floor(e/Math.pow(2,32)),n=e%Math.pow(2,32);this.bufferBuilder.append((4278190080&t)>>>24),this.bufferBuilder.append((16711680&t)>>>16),this.bufferBuilder.append((65280&t)>>>8),this.bufferBuilder.append(255&t),this.bufferBuilder.append((4278190080&n)>>>24),this.bufferBuilder.append((16711680&n)>>>16),this.bufferBuilder.append((65280&n)>>>8),this.bufferBuilder.append(255&n)}},{"./bufferbuilder":11}],11:[function(e,t,n){function i(){this._pieces=[],this._parts=[]}var r={};r.useBlobBuilder=function(){try{return new Blob([]),!1}catch(e){return!0}}(),r.useArrayBufferView=!r.useBlobBuilder&&function(){try{return 0===new Blob([new Uint8Array([])]).size}catch(e){return!0}}(),t.exports.binaryFeatures=r;var o=t.exports.BlobBuilder;"undefined"!=typeof window&&(o=t.exports.BlobBuilder=window.WebKitBlobBuilder||window.MozBlobBuilder||window.MSBlobBuilder||window.BlobBuilder),i.prototype.append=function(e){"number"==typeof e?this._pieces.push(e):(this.flush(),this._parts.push(e))},i.prototype.flush=function(){if(this._pieces.length>0){var e=new Uint8Array(this._pieces);r.useArrayBufferView||(e=e.buffer),this._parts.push(e),this._pieces=[]}},i.prototype.getBuffer=function(){if(this.flush(),r.useBlobBuilder){for(var e=new o,t=0,n=this._parts.length;n>t;t++)e.append(this._parts[t]);return e.getBlob()}return new Blob(this._parts)},t.exports.BufferBuilder=i},{}],12:[function(e,t,n){function i(e,t){return this instanceof i?(this._dc=e,r.debug=t,this._outgoing={},this._incoming={},this._received={},this._window=1e3,this._mtu=500,this._interval=0,this._count=0,this._queue=[],void this._setupDC()):new i(e)}var r=e("./util");i.prototype.send=function(e){var t=r.pack(e);return t.sizen;n+=1)e._intervalSend(t[n]);else e._intervalSend(t)},this._interval)},i.prototype._intervalSend=function(e){var t=this;e=r.pack(e),r.blobToBinaryString(e,function(e){t._dc.send(e)}),0===t._queue.length&&(clearTimeout(t._timeout),t._timeout=null)},i.prototype._processAcks=function(){for(var e in this._outgoing)this._outgoing.hasOwnProperty(e)&&this._sendWindowedChunks(e)},i.prototype._handleSend=function(e){for(var t=!0,n=0,i=this._queue.length;i>n;n+=1){var r=this._queue[n];r===e?t=!1:r._multiple&&-1!==r.indexOf(e)&&(t=!1)}t&&(this._queue.push(e),this._timeout||this._setupInterval())},i.prototype._setupDC=function(){var e=this;this._dc.onmessage=function(t){var n=t.data,i=n.constructor;if(i===String){var o=r.binaryStringToArrayBuffer(n);n=r.unpack(o),e._handleMessage(n)}}},i.prototype._handleMessage=function(e){var t,n=e[1],i=this._incoming[n],o=this._outgoing[n];switch(e[0]){case"no":var s=n;s&&this.onmessage(r.unpack(s));break;case"end":if(t=i,this._received[n]=e[2],!t)break;this._ack(n);break;case"ack":if(t=o){var a=e[2];t.ack=Math.max(a,t.ack),t.ack>=t.chunks.length?(r.log("Time: ",new Date-t.timer),delete this._outgoing[n]):this._processAcks()}break;case"chunk":if(t=i,!t){var c=this._received[n];if(c===!0)break;t={ack:["ack",n,0],chunks:[]},this._incoming[n]=t}var u=e[2],p=e[3];t.chunks[u]=new Uint8Array(p),u===t.ack[2]&&this._calculateNextAck(n),this._ack(n);break;default:this._handleSend(e)}},i.prototype._chunk=function(e){for(var t=[],n=e.size,i=0;n>i;){var o=Math.min(n,i+this._mtu),s=e.slice(i,o),a={payload:s};t.push(a),i=o}return r.log("Created",t.length,"chunks."),t},i.prototype._ack=function(e){var t=this._incoming[e].ack;this._received[e]===t[2]&&(this._complete(e),this._received[e]=!0),this._handleSend(t)},i.prototype._calculateNextAck=function(e){for(var t=this._incoming[e],n=t.chunks,i=0,r=n.length;r>i;i+=1)if(void 0===n[i])return void(t.ack[2]=i);t.ack[2]=n.length},i.prototype._sendWindowedChunks=function(e){r.log("sendWindowedChunks for: ",e);for(var t=this._outgoing[e],n=t.chunks,i=[],o=Math.min(t.ack+this._window,n.length),s=t.ack;o>s;s+=1)n[s].sent&&s!==t.ack||(n[s].sent=!0,i.push(["chunk",e,s,n[s].payload]));t.ack+this._window>=n.length&&i.push(["end",e,n.length]),i._multiple=!0,this._handleSend(i)},i.prototype._complete=function(e){r.log("Completed called for",e);var t=this,n=this._incoming[e].chunks,i=new Blob(n);r.blobToArrayBuffer(i,function(e){t.onmessage(r.unpack(e))}),delete this._incoming[e]},i.higherBandwidthSDP=function(e){var t=navigator.appVersion.match(/Chrome\/(.*?) /);if(t&&(t=parseInt(t[1].split(".").shift()),31>t)){var n=e.split("b=AS:30"),i="b=AS:102400";if(n.length>1)return n[0]+i+n[1]}return e},i.prototype.onmessage=function(e){},t.exports.Reliable=i},{"./util":13}],13:[function(e,t,n){var i=e("js-binarypack"),r={debug:!1,inherits:function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})},extend:function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e},pack:i.pack,unpack:i.unpack,log:function(){if(r.debug){for(var e=[],t=0;t