├── 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 |
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 |
4 |
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 |
4 |
5 |
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 | # 
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 | # 
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 |
73 |
74 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
123 |
124 |
136 |
137 |
138 |
Awaiting link generation
139 |
140 |
141 |
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