├── package.json ├── rtcmulticonnection-client ├── package.json ├── style.css ├── server.js ├── README.md └── index.html ├── datachannel-client ├── package.json ├── style.css ├── server.js ├── README.md ├── index.html └── DataChannel.js ├── videoconferencing-client ├── package.json ├── style.css ├── server.js ├── README.md ├── index.html ├── conference.js └── RTCPeerConnection-v1.5.js ├── reliable-signaler.js ├── signaler.js ├── index.js └── README.md /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reliable-signaler", 3 | "preferGlobal": true, 4 | "version": "1.0.2", 5 | "author": { 6 | "name": "Muaz Khan", 7 | "email": "muazkh@gmail.com", 8 | "url": "http://www.muazkhan.com/" 9 | }, 10 | "description": "Reliable signaling implementation for RTCMultiConnection.js, DataChannel.js and WebRTC Experiments.", 11 | "scripts": { 12 | "start": "node index.js" 13 | }, 14 | "main": "./index.js", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/muaz-khan/Reliable-Signaler.git" 18 | }, 19 | "keywords": [ 20 | "webrtc", 21 | "signaler", 22 | "signaling", 23 | "reliable", 24 | "javascript", 25 | "rtcmulticonnection", 26 | "webrtc-experiment", 27 | "muaz", 28 | "muaz-khan" 29 | ], 30 | "analyze": false, 31 | "license": "MIT", 32 | "readmeFilename": "README.md", 33 | "bugs": { 34 | "url": "https://github.com/muaz-khan/Reliable-Signaler/issues", 35 | "email": "muazkh@gmail.com" 36 | }, 37 | "homepage": "https://github.com/muaz-khan/WebRTC-Experiment", 38 | "_from": "reliable-signaler@" 39 | } 40 | -------------------------------------------------------------------------------- /rtcmulticonnection-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtcmulticonnection-client", 3 | "preferGlobal": true, 4 | "version": "1.0.5", 5 | "author": "Muaz Khan (http://www.muazkhan.com/)", 6 | "description": "Reliable signaling implementation for RTCMultiConnection.js", 7 | "scripts": { 8 | "start": "node server.js" 9 | }, 10 | "main": "./server.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/muaz-khan/Reliable-Signaler.git" 14 | }, 15 | "keywords": [ 16 | "webrtc", 17 | "signaler", 18 | "signaling", 19 | "reliable", 20 | "javascript", 21 | "rtcmulticonnection", 22 | "webrtc-experiment", 23 | "muaz", 24 | "muaz-khan" 25 | ], 26 | "analyze": false, 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/muaz-khan/Reliable-Signaler/issues", 30 | "email": "muazkh@gmail.com" 31 | }, 32 | "homepage": "http://www.RTCMultiConnection.org", 33 | "_from": "rtcmulticonnection-client@", 34 | "dependencies": { 35 | "reliable-signaler": "1.0.2", 36 | "socket.io": "0.9.x" 37 | }, 38 | "devDependencies": {} 39 | } 40 | -------------------------------------------------------------------------------- /datachannel-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datachannel-client", 3 | "preferGlobal": true, 4 | "version": "1.0.2", 5 | "author": "Muaz Khan (http://www.muazkhan.com/)", 6 | "description": "Reliable signaling implementation for DataChannel.js", 7 | "scripts": { 8 | "start": "node server.js" 9 | }, 10 | "main": "./server.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/muaz-khan/Reliable-Signaler.git" 14 | }, 15 | "keywords": [ 16 | "webrtc", 17 | "signaler", 18 | "signaling", 19 | "reliable", 20 | "javascript", 21 | "datachannel", 22 | "webrtc-experiment", 23 | "muaz", 24 | "muaz-khan" 25 | ], 26 | "analyze": false, 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/muaz-khan/Reliable-Signaler/issues", 30 | "email": "muazkh@gmail.com" 31 | }, 32 | "homepage": "https://github.com/muaz-khan/WebRTC-Experiment/blob/master/DataChannel", 33 | "_from": "datachannel-client@", 34 | "dependencies": { 35 | "reliable-signaler": "1.0.2", 36 | "socket.io": "0.9.x" 37 | }, 38 | "devDependencies": {} 39 | } 40 | -------------------------------------------------------------------------------- /videoconferencing-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videoconferencing-client", 3 | "preferGlobal": true, 4 | "version": "1.0.0", 5 | "author": "Muaz Khan (http://www.muazkhan.com/)", 6 | "description": "Reliable signaling implementation for video-conferencing WebRTC experiment.", 7 | "scripts": { 8 | "start": "node server.js" 9 | }, 10 | "main": "./server.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/muaz-khan/Reliable-Signaler.git" 14 | }, 15 | "keywords": [ 16 | "webrtc", 17 | "signaler", 18 | "signaling", 19 | "reliable", 20 | "javascript", 21 | "videoconferencing", 22 | "webrtc-experiment", 23 | "muaz", 24 | "muaz-khan" 25 | ], 26 | "analyze": false, 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/muaz-khan/Reliable-Signaler/issues", 30 | "email": "muazkh@gmail.com" 31 | }, 32 | "homepage": "https://github.com/muaz-khan/WebRTC-Experiment/blob/master/video-conferencing", 33 | "_from": "videoconferencing-client@", 34 | "dependencies": { 35 | "reliable-signaler": "1.0.2", 36 | "socket.io": "0.9.x" 37 | }, 38 | "devDependencies": {} 39 | } 40 | -------------------------------------------------------------------------------- /videoconferencing-client/style.css: -------------------------------------------------------------------------------- 1 | button, input, select { 2 | font-family: Myriad, Arial, Verdana; 3 | font-weight: normal; 4 | border-top-left-radius: 3px; 5 | border-top-right-radius: 3px; 6 | border-bottom-right-radius: 3px; 7 | border-bottom-left-radius: 3px; 8 | padding: 4px 12px; 9 | text-decoration: none; 10 | color: rgb(27, 26, 26); 11 | display: inline-block; 12 | box-shadow: rgb(255, 255, 255) 1px 1px 0px 0px inset; 13 | text-shadow: none; 14 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0.05, rgb(241, 241, 241)), to(rgb(230, 230, 230))); 15 | font-size: 20px; 16 | border: 1px solid red; 17 | outline:none; 18 | } 19 | button:active, input:active, select:active, button:focus, input:focus, select:focus { 20 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(5%, rgb(221, 221, 221)), to(rgb(250, 250, 250))); 21 | border: 1px solid rgb(142, 142, 142); 22 | } 23 | 24 | input, input:focus, input:active { 25 | background: white; 26 | } 27 | 28 | button[disabled], input[disabled], select[disabled] { 29 | background: rgb(249, 249, 249); 30 | border: 1px solid rgb(218, 207, 207); 31 | color: rgb(197, 189, 189); 32 | } 33 | 34 | video { 35 | width: 25%; 36 | } 37 | -------------------------------------------------------------------------------- /datachannel-client/style.css: -------------------------------------------------------------------------------- 1 | button, input, select { 2 | font-family: Myriad, Arial, Verdana; 3 | font-weight: normal; 4 | border-top-left-radius: 3px; 5 | border-top-right-radius: 3px; 6 | border-bottom-right-radius: 3px; 7 | border-bottom-left-radius: 3px; 8 | padding: 4px 12px; 9 | text-decoration: none; 10 | color: rgb(27, 26, 26); 11 | display: inline-block; 12 | box-shadow: rgb(255, 255, 255) 1px 1px 0px 0px inset; 13 | text-shadow: none; 14 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0.05, rgb(241, 241, 241)), to(rgb(230, 230, 230))); 15 | font-size: 20px; 16 | border: 1px solid red; 17 | outline:none; 18 | } 19 | button:active, input:active, select:active, button:focus, input:focus, select:focus { 20 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(5%, rgb(221, 221, 221)), to(rgb(250, 250, 250))); 21 | border: 1px solid rgb(142, 142, 142); 22 | } 23 | 24 | input, input:focus, input:active { 25 | background: white; 26 | } 27 | 28 | button[disabled], input[disabled], select[disabled] { 29 | background: rgb(249, 249, 249); 30 | border: 1px solid rgb(218, 207, 207); 31 | color: rgb(197, 189, 189); 32 | } 33 | 34 | .chat-output div { 35 | border-bottom: 1px solid black; 36 | padding: 3px 5px; 37 | } -------------------------------------------------------------------------------- /rtcmulticonnection-client/style.css: -------------------------------------------------------------------------------- 1 | button, input, select { 2 | font-family: Myriad, Arial, Verdana; 3 | font-weight: normal; 4 | border-top-left-radius: 3px; 5 | border-top-right-radius: 3px; 6 | border-bottom-right-radius: 3px; 7 | border-bottom-left-radius: 3px; 8 | padding: 4px 12px; 9 | text-decoration: none; 10 | color: rgb(27, 26, 26); 11 | display: inline-block; 12 | box-shadow: rgb(255, 255, 255) 1px 1px 0px 0px inset; 13 | text-shadow: none; 14 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0.05, rgb(241, 241, 241)), to(rgb(230, 230, 230))); 15 | font-size: 20px; 16 | border: 1px solid red; 17 | outline:none; 18 | } 19 | button:active, input:active, select:active, button:focus, input:focus, select:focus { 20 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(5%, rgb(221, 221, 221)), to(rgb(250, 250, 250))); 21 | border: 1px solid rgb(142, 142, 142); 22 | } 23 | 24 | input, input:focus, input:active { 25 | background: white; 26 | } 27 | 28 | button[disabled], input[disabled], select[disabled] { 29 | background: rgb(249, 249, 249); 30 | border: 1px solid rgb(218, 207, 207); 31 | color: rgb(197, 189, 189); 32 | } 33 | 34 | .chat-output div { 35 | border-bottom: 1px solid black; 36 | padding: 3px 5px; 37 | } 38 | 39 | video { 40 | width: 26%; 41 | } -------------------------------------------------------------------------------- /datachannel-client/server.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - www.MuazKhan.com 2 | // MIT License - www.WebRTC-Experiment.com/licence 3 | 4 | var path = require("path"), 5 | fs = require("fs"); 6 | 7 | var app = require('http').createServer(function (request, response) { 8 | var uri = require('url').parse(request.url).pathname, 9 | filename = path.join(process.cwd(), uri); 10 | 11 | fs.exists(filename, function (exists) { 12 | var contentType = { 13 | "Content-Type": "text/plain" 14 | }; 15 | 16 | if (!exists) { 17 | response.writeHead(404, contentType); 18 | response.write('404 Not Found: ' + filename + '\n'); 19 | response.end(); 20 | return; 21 | } 22 | 23 | if (fs.statSync(filename).isDirectory()) { 24 | contentType = { 25 | "Content-Type": "text/html" 26 | }; 27 | filename += '/index.html'; 28 | } 29 | 30 | fs.readFile(filename, 'binary', function (err, file) { 31 | if (err) { 32 | response.writeHead(500, contentType); 33 | response.write(err + "\n"); 34 | response.end(); 35 | return; 36 | } 37 | 38 | response.writeHead(200, contentType); 39 | response.write(file, 'binary'); 40 | response.end(); 41 | }); 42 | }); 43 | }); 44 | 45 | app.listen(8080); 46 | 47 | // npm install reliable-signaler 48 | require('reliable-signaler')(app); 49 | -------------------------------------------------------------------------------- /rtcmulticonnection-client/server.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - www.MuazKhan.com 2 | // MIT License - www.WebRTC-Experiment.com/licence 3 | 4 | var path = require("path"), 5 | fs = require("fs"); 6 | 7 | var app = require('http').createServer(function (request, response) { 8 | var uri = require('url').parse(request.url).pathname, 9 | filename = path.join(process.cwd(), uri); 10 | 11 | fs.exists(filename, function (exists) { 12 | var contentType = { 13 | "Content-Type": "text/plain" 14 | }; 15 | 16 | if (!exists) { 17 | response.writeHead(404, contentType); 18 | response.write('404 Not Found: ' + filename + '\n'); 19 | response.end(); 20 | return; 21 | } 22 | 23 | if (fs.statSync(filename).isDirectory()) { 24 | contentType = { 25 | "Content-Type": "text/html" 26 | }; 27 | filename += '/index.html'; 28 | } 29 | 30 | fs.readFile(filename, 'binary', function (err, file) { 31 | if (err) { 32 | response.writeHead(500, contentType); 33 | response.write(err + "\n"); 34 | response.end(); 35 | return; 36 | } 37 | 38 | response.writeHead(200, contentType); 39 | response.write(file, 'binary'); 40 | response.end(); 41 | }); 42 | }); 43 | }); 44 | 45 | app.listen(8080); 46 | 47 | // npm install reliable-signaler 48 | require('reliable-signaler')(app); 49 | -------------------------------------------------------------------------------- /videoconferencing-client/server.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - www.MuazKhan.com 2 | // MIT License - www.WebRTC-Experiment.com/licence 3 | 4 | var path = require("path"), 5 | fs = require("fs"); 6 | 7 | var app = require('http').createServer(function (request, response) { 8 | var uri = require('url').parse(request.url).pathname, 9 | filename = path.join(process.cwd(), uri); 10 | 11 | fs.exists(filename, function (exists) { 12 | var contentType = { 13 | "Content-Type": "text/plain" 14 | }; 15 | 16 | if (!exists) { 17 | response.writeHead(404, contentType); 18 | response.write('404 Not Found: ' + filename + '\n'); 19 | response.end(); 20 | return; 21 | } 22 | 23 | if (fs.statSync(filename).isDirectory()) { 24 | contentType = { 25 | "Content-Type": "text/html" 26 | }; 27 | filename += '/index.html'; 28 | } 29 | 30 | fs.readFile(filename, 'binary', function (err, file) { 31 | if (err) { 32 | response.writeHead(500, contentType); 33 | response.write(err + "\n"); 34 | response.end(); 35 | return; 36 | } 37 | 38 | response.writeHead(200, contentType); 39 | response.write(file, 'binary'); 40 | response.end(); 41 | }); 42 | }); 43 | }); 44 | 45 | app.listen(8080); 46 | 47 | // npm install reliable-signaler 48 | require('reliable-signaler')(app, { 49 | path: '/reliable-signaler/signaler.js' 50 | }); 51 | -------------------------------------------------------------------------------- /rtcmulticonnection-client/README.md: -------------------------------------------------------------------------------- 1 | # RTCMultiConnection client using [Reliable Signaler](https://github.com/muaz-khan/Reliable-Signaler) 2 | 3 | [![npm](https://img.shields.io/npm/v/rtcmulticonnection-client.svg)](https://npmjs.org/package/rtcmulticonnection-client) [![downloads](https://img.shields.io/npm/dm/rtcmulticonnection-client.svg)](https://npmjs.org/package/rtcmulticonnection-client) 4 | 5 | It is a node.js and socket.io based reliable signaling implementation for RTCMultiConnection.js 6 | 7 | ``` 8 | # install 9 | npm install rtcmulticonnection-client 10 | 11 | # run 12 | node ./node_modules/rtcmulticonnection-client/server.js 13 | ``` 14 | 15 | Now open localhost port:`8080`. 16 | 17 | # How it works? 18 | 19 | 1. You can store a room-id on server using `createNewRoomOnServer` method. 20 | 2. You can get that room-id using `getRoomFromServer` method. 21 | 22 | # How to use? 23 | 24 | 1. In your Node.js server, invoke `require('reliable-signaler')` and pass HTTP-Server object. 25 | 2. In your HTML file, link this script: `/reliable-signaler/signaler.js` 26 | 3. In your ` 14 | 15 | 16 | 93 | -------------------------------------------------------------------------------- /signaler.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | function initReliableSignaler(connection, socketURL) { 4 | var socket; 5 | 6 | if (!connection) throw '"connection" argument is required.'; 7 | 8 | function initSocket() { 9 | if (socket && connection && connection.isInitiator && connection.roomid) { 10 | socket.emit('keep-session', connection.roomid); 11 | } 12 | 13 | socket = io.connect(socketURL || '/'); 14 | socket.on('connect', function() { 15 | // if socket.io was disconnected out of network issues 16 | if (socket.isHavingError) { 17 | initSocket(); 18 | } 19 | }); 20 | socket.on('message', function(data) { 21 | if (onMessageCallbacks[data.channel]) { 22 | onMessageCallbacks[data.channel](data.message); 23 | }; 24 | }); 25 | socket.on('error', function() { 26 | socket.isHavingError = true; 27 | initSocket(); 28 | }); 29 | 30 | socket.on('disconnect', function() { 31 | socket.isHavingError = true; 32 | initSocket(); 33 | }); 34 | } 35 | initSocket(); 36 | 37 | var onMessageCallbacks = {}; 38 | 39 | // using socket.io for signaling 40 | connection.openSignalingChannel = function(config) { 41 | var channel = config.channel || this.channel || 'default-channel'; 42 | onMessageCallbacks[channel] = config.onmessage; 43 | if(config.onopen) setTimeout(config.onopen, 1); 44 | return { 45 | send: function(message) { 46 | socket.emit('message', { 47 | sender: connection.userid, 48 | channel: channel, 49 | message: message 50 | }); 51 | }, 52 | channel: channel 53 | }; 54 | }; 55 | 56 | function listenEventHandler(eventName, eventHandler) { 57 | window.removeEventListener(eventName, eventHandler); 58 | window.addEventListener(eventName, eventHandler, false); 59 | } 60 | 61 | listenEventHandler('load', onLineOffLineHandler); 62 | listenEventHandler('online', onLineOffLineHandler); 63 | listenEventHandler('offline', onLineOffLineHandler); 64 | 65 | function onLineOffLineHandler() { 66 | if (!navigator.onLine) { 67 | console.warn('Internet channel seems disconnected.'); 68 | return; 69 | } 70 | 71 | // if socket.io was disconnected out of network issues 72 | if (socket.isHavingError) { 73 | initSocket(); 74 | } 75 | } 76 | 77 | function getRandomString() { 78 | if (window.crypto && window.crypto.getRandomValues && navigator.userAgent.indexOf('Safari') === -1) { 79 | var a = window.crypto.getRandomValues(new Uint32Array(3)), 80 | token = ''; 81 | for (var i = 0, l = a.length; i < l; i++) { 82 | token += a[i].toString(36); 83 | } 84 | return token; 85 | } else { 86 | return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); 87 | } 88 | } 89 | 90 | return { 91 | socket: socket, 92 | createNewRoomOnServer: function(roomid, successCallback) { 93 | // for reusability on failures & reconnect 94 | connection.roomid = roomid; 95 | connection.isInitiator = true; 96 | connection.userid = connection.userid || getRandomString(); 97 | 98 | socket.emit('keep-in-server', roomid || connection.channel, successCallback || function() {}); 99 | }, 100 | getRoomFromServer: function(roomid, callback) { 101 | connection.userid = connection.userid || getRandomString(); 102 | socket.emit('get-session-info', roomid, callback); 103 | } 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /rtcmulticonnection-client/index.html: -------------------------------------------------------------------------------- 1 |
2 | /room-id// 3 | 4 | 5 |
6 |
7 |
8 |

9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 147 | -------------------------------------------------------------------------------- /videoconferencing-client/index.html: -------------------------------------------------------------------------------- 1 |
2 | /room-id// 3 | 4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 141 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: Code in this file is taken from socket.io github repository 2 | // It is added merely to provide direct access of this script: 3 | 4 | // 5 | 6 | // Remember, there is a separate file named as "reliable-signaler.js" 7 | // which handles socket.io signaling part. 8 | 9 | var http = require('http'); 10 | var read = require('fs').readFileSync; 11 | var parse = require('url').parse; 12 | var url = require('url'); 13 | 14 | module.exports = Server; 15 | 16 | function Server(srv, opts) { 17 | if (!(this instanceof Server)) return new Server(srv, opts); 18 | if ('object' == typeof srv && !srv.listen) { 19 | opts = srv; 20 | srv = null; 21 | } 22 | //console.log(opts); 23 | opts = opts || {}; 24 | this.nsps = {}; 25 | this.path(opts.path || '/reliable-signaler/signaler.js'); 26 | this.serveClient(false !== opts.serveClient); 27 | this.origins(opts.origins || '*:*'); 28 | if (srv) this.attach(srv, opts); 29 | } 30 | 31 | Server.prototype.path = function(v) { 32 | if (!arguments.length) return this._path; 33 | this._path = v.replace(/\/$/, ''); 34 | return this; 35 | }; 36 | 37 | Server.prototype.checkRequest = function(req, fn) { 38 | var origin = req.headers.origin || req.headers.referer; 39 | 40 | // file:// URLs produce a null Origin which can't be authorized via echo-back 41 | if ('null' == origin) origin = '*'; 42 | 43 | if (!!origin && typeof(this._origins) == 'function') return this._origins(origin, fn); 44 | if (this._origins.indexOf('*:*') !== -1) return fn(null, true); 45 | if (origin) { 46 | try { 47 | var parts = url.parse(origin); 48 | parts.port = parts.port || 80; 49 | var ok = ~this._origins.indexOf(parts.hostname + ':' + parts.port) || 50 | ~this._origins.indexOf(parts.hostname + ':*') || 51 | ~this._origins.indexOf('*:' + parts.port); 52 | return fn(null, !!ok); 53 | } catch (ex) {} 54 | } 55 | fn(null, false); 56 | }; 57 | 58 | Server.prototype.serveClient = function(v) { 59 | if (!arguments.length) return this._serveClient; 60 | this._serveClient = v; 61 | return this; 62 | }; 63 | 64 | var oldSettings = { 65 | "transports": "transports", 66 | "heartbeat timeout": "pingTimeout", 67 | "heartbeat interval": "pingInterval", 68 | "destroy buffer size": "maxHttpBufferSize" 69 | }; 70 | 71 | Server.prototype.set = function(key, val) { 72 | if ('authorization' == key && val) { 73 | this.use(function(socket, next) { 74 | val(socket.request, function(err, authorized) { 75 | if (err) return next(new Error(err)); 76 | if (!authorized) return next(new Error('Not authorized')); 77 | next(); 78 | }); 79 | }); 80 | } else if ('origins' == key && val) { 81 | this.origins(val); 82 | } else if ('resource' == key) { 83 | this.path(val); 84 | } else if (oldSettings[key] && this.eio[oldSettings[key]]) { 85 | this.eio[oldSettings[key]] = val; 86 | } else { 87 | console.error('Option %s is not valid. Please refer to the README.', key); 88 | } 89 | 90 | return this; 91 | }; 92 | 93 | Server.prototype.origins = function(v) { 94 | if (!arguments.length) return this._origins; 95 | 96 | this._origins = v; 97 | return this; 98 | }; 99 | 100 | Server.prototype.listen = 101 | Server.prototype.attach = function(srv, opts) { 102 | if ('function' == typeof srv) { 103 | var msg = 'You are trying to attach reliable-signaler to an express' + 104 | 'request handler function. Please pass a http.Server instance.'; 105 | throw new Error(msg); 106 | } 107 | 108 | // handle a port as a string 109 | if (Number(srv) == srv) { 110 | srv = Number(srv); 111 | } 112 | 113 | if ('number' == typeof srv) { 114 | var port = srv; 115 | srv = http.Server(function(req, res) { 116 | res.writeHead(404); 117 | res.end(); 118 | }); 119 | srv.listen(port); 120 | } 121 | 122 | // set engine.io path to `/reliable-signaler` 123 | opts = opts || {}; 124 | opts.path = opts.path || this.path(); 125 | // set origins verification 126 | opts.allowRequest = this.checkRequest.bind(this); 127 | 128 | // attach static file serving 129 | if (this._serveClient) this.attachServe(srv); 130 | 131 | // Export http server 132 | this.httpServer = srv; 133 | 134 | require('./reliable-signaler.js').ReliableSignaler(srv, opts.socketCallback || function() {}); 135 | 136 | return this; 137 | }; 138 | 139 | Server.prototype.attachServe = function(srv) { 140 | var url = this._path; 141 | var evs = srv.listeners('request').slice(0); 142 | var self = this; 143 | srv.removeAllListeners('request'); 144 | srv.on('request', function(req, res) { 145 | if (0 == req.url.indexOf(url)) { 146 | self.serve(req, res); 147 | } else { 148 | for (var i = 0; i < evs.length; i++) { 149 | evs[i].call(srv, req, res); 150 | } 151 | } 152 | }); 153 | }; 154 | 155 | Server.prototype.serve = function(req, res) { 156 | res.setHeader('Content-Type', 'application/javascript'); 157 | res.writeHead(200); 158 | res.end(read('node_modules' + this._path, 'utf-8')); 159 | }; 160 | 161 | Server.listen = Server; 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reliable Signaler [![npm](https://img.shields.io/npm/v/reliable-signaler.svg)](https://npmjs.org/package/reliable-signaler) [![downloads](https://img.shields.io/npm/dm/reliable-signaler.svg)](https://npmjs.org/package/reliable-signaler) 2 | 3 | It is a node.js and socket.io based reliable signaling implementation. Remember, reliable doesn't mean "scalable"; reliable simply means that it auto reconnects in any kind of failure or internet disconnect. It is having following features: 4 | 5 | 1. Auto reconnects if node.js gets down out of any reason. 6 | 2. Auto reconnects if internet connection disconnects. 7 | 3. It provides [custom-signaling](https://github.com/muaz-khan/RTCMultiConnection/wiki/Custom-Private-Servers#signaling-servers) for your [RTCMultiConnection](https://github.com/muaz-khan/RTCMultiConnection) and [DataChannel](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/DataChannel) applications! 8 | 9 | ``` 10 | npm install reliable-signaler 11 | ``` 12 | 13 | # How it works? 14 | 15 | 1. You can store a room-id on server using `createNewRoomOnServer` method. 16 | 2. You can get that room-id using `getRoomFromServer` method. 17 | 18 | # How to use? 19 | 20 | 1. In your Node.js server, invoke `require('reliable-signaler')` and pass HTTP-Server object. 21 | 2. In your HTML file, link this script: `/reliable-signaler/signaler.js` 22 | 3. In your ` 82 | ``` 83 | 84 | And your client-side javascript code: 85 | 86 | ```javascript 87 | var connection = new RTCMultiConnection(); 88 | 89 | // invoke "initReliableSignaler" and pass "connection" or "channel" object 90 | var signaler = initReliableSignaler(connection, 'http://domain:port/'); 91 | ``` 92 | 93 | Call `createNewRoomOnServer` method as soon as you'll call `open` method. You can even call `createNewRoomOnServer` earlier than `open` however it isn't suggested: 94 | 95 | For RTCMultiConnection: 96 | 97 | ```javascript 98 | // for data-only sessions 99 | connection.open(); 100 | signaler.createNewRoomOnServer(connection.sessionid); 101 | 102 | // or (not recommended) 103 | signaler.createNewRoomOnServer(connection.sessionid, function() { 104 | connection.open(); 105 | }); 106 | 107 | // or --- recommended. 108 | connection.open({ 109 | onMediaCaptured: function() { 110 | signaler.createNewRoomOnServer(connection.sessionid); 111 | } 112 | }); 113 | ``` 114 | 115 | For DataChannel: 116 | 117 | ```javascript 118 | channel.open('room-id'); 119 | signaler.createNewRoomOnServer('room-id', successCallback); 120 | ``` 121 | 122 | For participants, call `getRoomFromServer` method: 123 | 124 | ```javascript 125 | // RTCMultiConnection 126 | signaler.getRoomFromServer('sessioin-id', function(roomid) { 127 | // invoke "join" in callback 128 | connection.join({ 129 | sessionid: roomid, 130 | userid: roomid, 131 | extra: {}, 132 | session: connection.session 133 | }); 134 | 135 | // or simply 136 | connection.join(roomid); 137 | 138 | // or 139 | connection.connect(roomid); 140 | }); 141 | 142 | // DataChannel 143 | signaler.getRoomFromServer('sessioin-id', function(roomid) { 144 | channel.join({ 145 | id: roomid, 146 | owner: roomid 147 | }); 148 | 149 | // or 150 | channel.connect(roomid); 151 | }); 152 | ``` 153 | 154 | # Complete Client-Side Example for RTCMultiConnection 155 | 156 | ```html 157 | 158 | 184 | ``` 185 | 186 | # Complete Client-Side Example for DataChannel 187 | 188 | ```html 189 | 190 | 212 | ``` 213 | 214 | ## API Reference 215 | 216 | Constructor takes either `RTCMultiConnection` instance or a `config` object: 217 | 218 | ```javascript 219 | # 1st option: Pass RTCMultiConnection object 220 | var signaler = initReliableSignaler(connection); 221 | 222 | # 2nd option: Pass "config" object 223 | var signaler = initReliableSignaler(connection, '/'); 224 | ``` 225 | 226 | `initReliableSignaler` global-function exposes/returns 3-objects: 227 | 228 | 1. `socket` 229 | 2. `createNewRoomOnServer` 230 | 3. `getRoomFromServer` 231 | 232 | ```javascript 233 | // "socket" object 234 | signaler.socket.emit('message', 'hello'); 235 | 236 | // "createNewRoomOnServer" method 237 | signaler.createNewRoomOnServer(connection.sessionid, successCallback); 238 | 239 | // "getRoomFromServer" object 240 | signaler.getRoomFromServer('sessioin-id', callback); 241 | ``` 242 | 243 | ## `createNewRoomOnServer` 244 | 245 | This method simply takes `sessioin-id` and stores in node.js server. You can even pass `successCallback`. 246 | 247 | ```javascript 248 | signaler.createNewRoomOnServer(roomid, successCallback); 249 | ``` 250 | 251 | ## `getRoomFromServer` 252 | 253 | This method looks for active `sessioin-id` in node.js server. Node.js server will fire callback only when session is found. 254 | 255 | If session is absent, then node.js server will wait until someone opens that session; and node.js will fire `getRoomFromServer-callback` as soon a session is opened. 256 | 257 | ```javascript 258 | signaler.getRoomFromServer(roomid, successCallback); 259 | ``` 260 | 261 | ## License 262 | 263 | [Reliable-Signaler](https://github.com/muaz-khan/Reliable-Signaler) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). 264 | -------------------------------------------------------------------------------- /videoconferencing-client/conference.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - www.MuazKhan.com 2 | // MIT License - www.WebRTC-Experiment.com/licence 3 | // Experiments - github.com/muaz-khan/WebRTC-Experiment 4 | 5 | // This library is known as multi-user connectivity wrapper! 6 | // It handles connectivity tasks to make sure two or more users can interconnect! 7 | 8 | var conference = function(config) { 9 | var self = { 10 | userToken: uniqueToken() 11 | }; 12 | 13 | var channels = '--', 14 | isbroadcaster; 15 | var isGetNewRoom = true; 16 | var sockets = []; 17 | var defaultSocket = {}; 18 | 19 | function openDefaultSocket() { 20 | defaultSocket = config.openSocket({ 21 | onmessage: onDefaultSocketResponse, 22 | callback: function(socket) { 23 | defaultSocket = socket; 24 | } 25 | }); 26 | } 27 | 28 | function onDefaultSocketResponse(response) { 29 | if (response.userToken == self.userToken) return; 30 | 31 | if (isGetNewRoom && response.roomToken && response.broadcaster) config.onRoomFound(response); 32 | 33 | if (response.newParticipant && self.joinedARoom && self.broadcasterid == response.userToken) onNewParticipant(response.newParticipant); 34 | 35 | if (response.userToken && response.joinUser == self.userToken && response.participant && channels.indexOf(response.userToken) == -1) { 36 | channels += response.userToken + '--'; 37 | openSubSocket({ 38 | isofferer: true, 39 | channel: response.channel || response.userToken 40 | }); 41 | } 42 | 43 | // to make sure room is unlisted if owner leaves 44 | if (response.left && config.onRoomClosed) { 45 | config.onRoomClosed(response); 46 | } 47 | } 48 | 49 | function openSubSocket(_config) { 50 | if (!_config.channel) return; 51 | var socketConfig = { 52 | channel: _config.channel, 53 | onmessage: socketResponse, 54 | onopen: function() { 55 | if (isofferer && !peer) initPeer(); 56 | sockets[sockets.length] = socket; 57 | } 58 | }; 59 | 60 | socketConfig.callback = function(_socket) { 61 | socket = _socket; 62 | this.onopen(); 63 | }; 64 | 65 | var socket = config.openSocket(socketConfig), 66 | isofferer = _config.isofferer, 67 | gotstream, 68 | video = document.createElement('video'), 69 | inner = {}, 70 | peer; 71 | 72 | var peerConfig = { 73 | attachStream: config.attachStream, 74 | onICE: function(candidate) { 75 | socket.send({ 76 | userToken: self.userToken, 77 | candidate: { 78 | sdpMLineIndex: candidate.sdpMLineIndex, 79 | candidate: JSON.stringify(candidate.candidate) 80 | } 81 | }); 82 | }, 83 | onRemoteStream: function(stream) { 84 | if (!stream) return; 85 | 86 | video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : webkitURL.createObjectURL(stream); 87 | video.play(); 88 | 89 | _config.stream = stream; 90 | onRemoteStreamStartsFlowing(); 91 | }, 92 | onRemoteStreamEnded: function(stream) { 93 | if (config.onRemoteStreamEnded) 94 | config.onRemoteStreamEnded(stream, video); 95 | } 96 | }; 97 | 98 | function initPeer(offerSDP) { 99 | if (!offerSDP) { 100 | peerConfig.onOfferSDP = sendsdp; 101 | } else { 102 | peerConfig.offerSDP = offerSDP; 103 | peerConfig.onAnswerSDP = sendsdp; 104 | } 105 | 106 | peer = RTCPeerConnection(peerConfig); 107 | } 108 | 109 | function afterRemoteStreamStartedFlowing() { 110 | gotstream = true; 111 | 112 | if (config.onRemoteStream) 113 | config.onRemoteStream({ 114 | video: video, 115 | stream: _config.stream 116 | }); 117 | 118 | if (isbroadcaster && channels.split('--').length > 3) { 119 | /* broadcasting newly connected participant for video-conferencing! */ 120 | defaultSocket.send({ 121 | newParticipant: socket.channel, 122 | userToken: self.userToken 123 | }); 124 | } 125 | } 126 | 127 | function onRemoteStreamStartsFlowing() { 128 | if (navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i)) { 129 | // if mobile device 130 | return afterRemoteStreamStartedFlowing(); 131 | } 132 | 133 | if (!(video.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || video.paused || video.currentTime <= 0)) { 134 | afterRemoteStreamStartedFlowing(); 135 | } else setTimeout(onRemoteStreamStartsFlowing, 50); 136 | } 137 | 138 | function sendsdp(sdp) { 139 | socket.send({ 140 | userToken: self.userToken, 141 | sdp: JSON.stringify(sdp) 142 | }); 143 | } 144 | 145 | function socketResponse(response) { 146 | if (response.userToken == self.userToken) return; 147 | if (response.sdp) { 148 | inner.sdp = JSON.parse(response.sdp); 149 | selfInvoker(); 150 | } 151 | 152 | if (response.candidate && !gotstream) { 153 | if (!peer) console.error('missed an ice', response.candidate); 154 | else 155 | peer.addICE({ 156 | sdpMLineIndex: response.candidate.sdpMLineIndex, 157 | candidate: JSON.parse(response.candidate.candidate) 158 | }); 159 | } 160 | 161 | if (response.left) { 162 | if (peer && peer.peer) { 163 | peer.peer.close(); 164 | peer.peer = null; 165 | } 166 | } 167 | } 168 | 169 | var invokedOnce = false; 170 | 171 | function selfInvoker() { 172 | if (invokedOnce) return; 173 | 174 | invokedOnce = true; 175 | 176 | if (isofferer) peer.addAnswerSDP(inner.sdp); 177 | else initPeer(inner.sdp); 178 | } 179 | } 180 | 181 | function leave() { 182 | var length = sockets.length; 183 | for (var i = 0; i < length; i++) { 184 | var socket = sockets[i]; 185 | if (socket) { 186 | socket.send({ 187 | left: true, 188 | userToken: self.userToken 189 | }); 190 | delete sockets[i]; 191 | } 192 | } 193 | 194 | // if owner leaves; try to remove his room from all other users side 195 | if (isbroadcaster) { 196 | defaultSocket.send({ 197 | left: true, 198 | userToken: self.userToken, 199 | roomToken: self.roomToken 200 | }); 201 | } 202 | 203 | if (config.attachStream) config.attachStream.stop(); 204 | } 205 | 206 | window.addEventListener('beforeunload', function() { 207 | leave(); 208 | }, false); 209 | 210 | window.addEventListener('keyup', function(e) { 211 | if (e.keyCode == 116) 212 | leave(); 213 | }, false); 214 | 215 | function startBroadcasting(transmitRoomOnce) { 216 | defaultSocket && defaultSocket.send({ 217 | roomToken: self.roomToken, 218 | roomName: self.roomName, 219 | broadcaster: self.userToken 220 | }); 221 | if (transmitRoomOnce) return; 222 | setTimeout(startBroadcasting, 3000); 223 | } 224 | 225 | function onNewParticipant(channel) { 226 | if (!channel || channels.indexOf(channel) != -1 || channel == self.userToken) return; 227 | channels += channel + '--'; 228 | 229 | var new_channel = uniqueToken(); 230 | openSubSocket({ 231 | channel: new_channel 232 | }); 233 | 234 | defaultSocket.send({ 235 | participant: true, 236 | userToken: self.userToken, 237 | joinUser: channel, 238 | channel: new_channel 239 | }); 240 | } 241 | 242 | function uniqueToken() { 243 | var s4 = function() { 244 | return Math.floor(Math.random() * 0x10000).toString(16); 245 | }; 246 | return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4(); 247 | } 248 | 249 | openDefaultSocket(); 250 | return { 251 | createRoom: function(_config) { 252 | _config = _config || {}; 253 | 254 | self.roomName = _config.roomName || 'Anonymous'; 255 | self.roomToken = _config.roomToken || uniqueToken(); 256 | 257 | if (_config.userToken) { 258 | self.userToken = _config.userToken; 259 | } 260 | 261 | isbroadcaster = true; 262 | isGetNewRoom = false; 263 | startBroadcasting(_config.transmitRoomOnce); 264 | }, 265 | joinRoom: function(_config) { 266 | _config = _config || {}; 267 | 268 | self.roomToken = _config.roomToken; 269 | isGetNewRoom = false; 270 | 271 | self.joinedARoom = true; 272 | self.broadcasterid = _config.joinUser; 273 | 274 | openSubSocket({ 275 | channel: self.userToken 276 | }); 277 | 278 | defaultSocket.send({ 279 | participant: true, 280 | userToken: self.userToken, 281 | joinUser: _config.joinUser 282 | }); 283 | }, 284 | leaveRoom: leave 285 | }; 286 | }; 287 | -------------------------------------------------------------------------------- /videoconferencing-client/RTCPeerConnection-v1.5.js: -------------------------------------------------------------------------------- 1 | // Last time updated at 26 Feb 2014, 08:32:23 2 | 3 | // Muaz Khan - github.com/muaz-khan 4 | // MIT License - www.WebRTC-Experiment.com/licence 5 | // Documentation - github.com/muaz-khan/WebRTC-Experiment/tree/master/RTCPeerConnection 6 | 7 | window.moz = !!navigator.mozGetUserMedia; 8 | var chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]); 9 | 10 | function RTCPeerConnection(options) { 11 | var w = window, 12 | PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, 13 | SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, 14 | IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; 15 | 16 | var iceServers = []; 17 | 18 | if (moz) { 19 | iceServers.push({ 20 | url: 'stun:23.21.150.121' 21 | }); 22 | 23 | iceServers.push({ 24 | url: 'stun:stun.services.mozilla.com' 25 | }); 26 | } 27 | 28 | if (!moz) { 29 | iceServers.push({ 30 | url: 'stun:stun.l.google.com:19302' 31 | }); 32 | 33 | iceServers.push({ 34 | url: 'stun:stun.anyfirewall.com:3478' 35 | }); 36 | } 37 | 38 | if (!moz && chromeVersion < 28) { 39 | iceServers.push({ 40 | url: 'turn:homeo@turn.bistri.com:80', 41 | credential: 'homeo' 42 | }); 43 | } 44 | 45 | if (!moz && chromeVersion >= 28) { 46 | iceServers.push({ 47 | url: 'turn:turn.bistri.com:80', 48 | credential: 'homeo', 49 | username: 'homeo' 50 | }); 51 | 52 | iceServers.push({ 53 | url: 'turn:turn.anyfirewall.com:443?transport=tcp', 54 | credential: 'webrtc', 55 | username: 'webrtc' 56 | }); 57 | } 58 | 59 | if (options.iceServers) iceServers = options.iceServers; 60 | 61 | iceServers = { 62 | iceServers: iceServers 63 | }; 64 | 65 | console.debug('ice-servers', JSON.stringify(iceServers.iceServers, null, '\t')); 66 | 67 | var optional = { 68 | optional: [] 69 | }; 70 | 71 | if (!moz) { 72 | optional.optional = [{ 73 | DtlsSrtpKeyAgreement: true 74 | }]; 75 | 76 | if (options.onChannelMessage) 77 | optional.optional = [{ 78 | RtpDataChannels: true 79 | }]; 80 | } 81 | 82 | console.debug('optional-arguments', JSON.stringify(optional.optional, null, '\t')); 83 | 84 | var peer = new PeerConnection(iceServers, optional); 85 | 86 | openOffererChannel(); 87 | 88 | peer.onicecandidate = function(event) { 89 | if (event.candidate) 90 | options.onICE(event.candidate); 91 | }; 92 | 93 | // attachStream = MediaStream; 94 | if (options.attachStream) peer.addStream(options.attachStream); 95 | 96 | // attachStreams[0] = audio-stream; 97 | // attachStreams[1] = video-stream; 98 | // attachStreams[2] = screen-capturing-stream; 99 | if (options.attachStreams && options.attachStream.length) { 100 | var streams = options.attachStreams; 101 | for (var i = 0; i < streams.length; i++) { 102 | peer.addStream(streams[i]); 103 | } 104 | } 105 | 106 | peer.onaddstream = function(event) { 107 | var remoteMediaStream = event.stream; 108 | 109 | // onRemoteStreamEnded(MediaStream) 110 | remoteMediaStream.onended = function() { 111 | if (options.onRemoteStreamEnded) options.onRemoteStreamEnded(remoteMediaStream); 112 | }; 113 | 114 | // onRemoteStream(MediaStream) 115 | if (options.onRemoteStream) options.onRemoteStream(remoteMediaStream); 116 | 117 | console.debug('on:add:stream', remoteMediaStream); 118 | }; 119 | 120 | var constraints = options.constraints || { 121 | optional: [], 122 | mandatory: { 123 | OfferToReceiveAudio: true, 124 | OfferToReceiveVideo: true 125 | } 126 | }; 127 | 128 | console.debug('sdp-constraints', JSON.stringify(constraints.mandatory, null, '\t')); 129 | 130 | // onOfferSDP(RTCSessionDescription) 131 | 132 | function createOffer() { 133 | if (!options.onOfferSDP) return; 134 | 135 | peer.createOffer(function(sessionDescription) { 136 | sessionDescription.sdp = setBandwidth(sessionDescription.sdp); 137 | peer.setLocalDescription(sessionDescription); 138 | options.onOfferSDP(sessionDescription); 139 | 140 | console.debug('offer-sdp', sessionDescription.sdp); 141 | }, onSdpError, constraints); 142 | } 143 | 144 | // onAnswerSDP(RTCSessionDescription) 145 | 146 | function createAnswer() { 147 | if (!options.onAnswerSDP) return; 148 | 149 | //options.offerSDP.sdp = addStereo(options.offerSDP.sdp); 150 | console.debug('offer-sdp', options.offerSDP.sdp); 151 | peer.setRemoteDescription(new SessionDescription(options.offerSDP), onSdpSuccess, onSdpError); 152 | peer.createAnswer(function(sessionDescription) { 153 | sessionDescription.sdp = setBandwidth(sessionDescription.sdp); 154 | peer.setLocalDescription(sessionDescription); 155 | options.onAnswerSDP(sessionDescription); 156 | console.debug('answer-sdp', sessionDescription.sdp); 157 | }, onSdpError, constraints); 158 | } 159 | 160 | // if Mozilla Firefox & DataChannel; offer/answer will be created later 161 | if ((options.onChannelMessage && !moz) || !options.onChannelMessage) { 162 | createOffer(); 163 | createAnswer(); 164 | } 165 | 166 | // options.bandwidth = { audio: 50, video: 256, data: 30 * 1000 * 1000 } 167 | var bandwidth = options.bandwidth; 168 | 169 | function setBandwidth(sdp) { 170 | if (moz || !bandwidth /* || navigator.userAgent.match( /Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i ) */) return sdp; 171 | 172 | // remove existing bandwidth lines 173 | sdp = sdp.replace( /b=AS([^\r\n]+\r\n)/g , ''); 174 | 175 | if (bandwidth.audio) { 176 | sdp = sdp.replace( /a=mid:audio\r\n/g , 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); 177 | } 178 | 179 | if (bandwidth.video) { 180 | sdp = sdp.replace( /a=mid:video\r\n/g , 'a=mid:video\r\nb=AS:' + bandwidth.video + '\r\n'); 181 | } 182 | 183 | if (bandwidth.data) { 184 | sdp = sdp.replace( /a=mid:data\r\n/g , 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); 185 | } 186 | 187 | return sdp; 188 | } 189 | 190 | // DataChannel management 191 | var channel; 192 | 193 | function openOffererChannel() { 194 | if (!options.onChannelMessage || (moz && !options.onOfferSDP)) 195 | return; 196 | 197 | _openOffererChannel(); 198 | 199 | if (!moz) return; 200 | navigator.mozGetUserMedia({ 201 | audio: true, 202 | fake: true 203 | }, function(stream) { 204 | peer.addStream(stream); 205 | createOffer(); 206 | }, useless); 207 | } 208 | 209 | function _openOffererChannel() { 210 | channel = peer.createDataChannel(options.channel || 'RTCDataChannel', moz ? { } : { 211 | reliable: false // Deprecated 212 | }); 213 | 214 | if (moz) channel.binaryType = 'blob'; 215 | 216 | setChannelEvents(); 217 | } 218 | 219 | function setChannelEvents() { 220 | channel.onmessage = function(event) { 221 | if (options.onChannelMessage) options.onChannelMessage(event); 222 | }; 223 | 224 | channel.onopen = function() { 225 | if (options.onChannelOpened) options.onChannelOpened(channel); 226 | }; 227 | channel.onclose = function(event) { 228 | if (options.onChannelClosed) options.onChannelClosed(event); 229 | 230 | console.warn('WebRTC DataChannel closed', event); 231 | }; 232 | channel.onerror = function(event) { 233 | if (options.onChannelError) options.onChannelError(event); 234 | 235 | console.error('WebRTC DataChannel error', event); 236 | }; 237 | } 238 | 239 | if (options.onAnswerSDP && moz && options.onChannelMessage) 240 | openAnswererChannel(); 241 | 242 | function openAnswererChannel() { 243 | peer.ondatachannel = function(event) { 244 | channel = event.channel; 245 | channel.binaryType = 'blob'; 246 | setChannelEvents(); 247 | }; 248 | 249 | if (!moz) return; 250 | navigator.mozGetUserMedia({ 251 | audio: true, 252 | fake: true 253 | }, function(stream) { 254 | peer.addStream(stream); 255 | createAnswer(); 256 | }, useless); 257 | } 258 | 259 | // fake:true is also available on chrome under a flag! 260 | 261 | function useless() { 262 | console.error('Error in fake:true'); 263 | } 264 | 265 | function onSdpSuccess() { 266 | } 267 | 268 | function onSdpError(e) { 269 | var message = JSON.stringify(e, null, '\t'); 270 | 271 | if (message.indexOf('RTP/SAVPF Expects at least 4 fields') != -1) { 272 | message = 'It seems that you are trying to interop RTP-datachannels with SCTP. It is not supported!'; 273 | } 274 | 275 | console.error('onSdpError:', message); 276 | } 277 | 278 | return { 279 | addAnswerSDP: function(sdp) { 280 | console.debug('adding answer-sdp', sdp.sdp); 281 | peer.setRemoteDescription(new SessionDescription(sdp), onSdpSuccess, onSdpError); 282 | }, 283 | addICE: function(candidate) { 284 | peer.addIceCandidate(new IceCandidate({ 285 | sdpMLineIndex: candidate.sdpMLineIndex, 286 | candidate: candidate.candidate 287 | })); 288 | 289 | console.debug('adding-ice', candidate.candidate); 290 | }, 291 | 292 | peer: peer, 293 | channel: channel, 294 | sendData: function(message) { 295 | channel && channel.send(message); 296 | } 297 | }; 298 | } 299 | 300 | // getUserMedia 301 | var video_constraints = { 302 | mandatory: { }, 303 | optional: [] 304 | }; 305 | 306 | function getUserMedia(options) { 307 | var n = navigator, 308 | media; 309 | n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; 310 | n.getMedia(options.constraints || { 311 | audio: true, 312 | video: video_constraints 313 | }, streaming, options.onerror || function(e) { 314 | console.error(e); 315 | }); 316 | 317 | function streaming(stream) { 318 | var video = options.video; 319 | if (video) { 320 | video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); 321 | video.play(); 322 | } 323 | options.onsuccess && options.onsuccess(stream); 324 | media = stream; 325 | } 326 | 327 | return media; 328 | } 329 | -------------------------------------------------------------------------------- /datachannel-client/DataChannel.js: -------------------------------------------------------------------------------- 1 | // Last time updated at Nov 21, 2014, 08:32:23 2 | 3 | // Muaz Khan - www.MuazKhan.com 4 | // MIT License - www.WebRTC-Experiment.com/licence 5 | // Documentation - github.com/muaz-khan/WebRTC-Experiment/tree/master/DataChannel 6 | // ______________ 7 | // DataChannel.js 8 | 9 | (function () { 10 | window.DataChannel = function (channel, extras) { 11 | if (channel) this.automatic = true; 12 | this.channel = channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); 13 | 14 | extras = extras || {}; 15 | 16 | var self = this, 17 | dataConnector, fileReceiver, textReceiver; 18 | 19 | this.onmessage = function (message, userid) { 20 | console.debug(userid, 'sent message:', message); 21 | }; 22 | 23 | this.channels = {}; 24 | this.onopen = function (userid) { 25 | console.debug(userid, 'is connected with you.'); 26 | }; 27 | 28 | this.onclose = function (event) { 29 | console.error('data channel closed:', event); 30 | }; 31 | 32 | this.onerror = function (event) { 33 | console.error('data channel error:', event); 34 | }; 35 | 36 | // by default; received file will be auto-saved to disk 37 | this.autoSaveToDisk = true; 38 | this.onFileReceived = function (fileName) { 39 | console.debug('File <', fileName, '> received successfully.'); 40 | }; 41 | 42 | this.onFileSent = function (file) { 43 | console.debug('File <', file.name, '> sent successfully.'); 44 | }; 45 | 46 | this.onFileProgress = function (packets) { 47 | console.debug('<', packets.remaining, '> items remaining.'); 48 | }; 49 | 50 | function prepareInit(callback) { 51 | for (var extra in extras) { 52 | self[extra] = extras[extra]; 53 | } 54 | self.direction = self.direction || 'many-to-many'; 55 | if (self.userid) window.userid = self.userid; 56 | 57 | if (!self.openSignalingChannel) { 58 | if (typeof self.transmitRoomOnce == 'undefined') self.transmitRoomOnce = true; 59 | 60 | // socket.io over node.js: https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md 61 | self.openSignalingChannel = function (config) { 62 | config = config || {}; 63 | 64 | channel = config.channel || self.channel || 'default-channel'; 65 | var socket = new window.Firebase('https://' + (self.firebase || 'chat') + '.firebaseIO.com/' + channel); 66 | socket.channel = channel; 67 | 68 | socket.on('child_added', function (data) { 69 | config.onmessage(data.val()); 70 | }); 71 | 72 | socket.send = function (data) { 73 | this.push(data); 74 | }; 75 | 76 | if (!self.socket) self.socket = socket; 77 | if (channel != self.channel || (self.isInitiator && channel == self.channel)) 78 | socket.onDisconnect().remove(); 79 | 80 | if (config.onopen) setTimeout(config.onopen, 1); 81 | 82 | return socket; 83 | }; 84 | 85 | if (!window.Firebase) { 86 | var script = document.createElement('script'); 87 | script.src = 'https://www.webrtc-experiment.com/firebase.js'; 88 | script.onload = callback; 89 | document.documentElement.appendChild(script); 90 | } else callback(); 91 | } else callback(); 92 | } 93 | 94 | function init() { 95 | if (self.config) return; 96 | 97 | self.config = { 98 | ondatachannel: function (room) { 99 | if (!dataConnector) { 100 | self.room = room; 101 | return; 102 | } 103 | 104 | var tempRoom = { 105 | id: room.roomToken, 106 | owner: room.broadcaster 107 | }; 108 | 109 | if (self.ondatachannel) return self.ondatachannel(tempRoom); 110 | 111 | if (self.joinedARoom) return; 112 | self.joinedARoom = true; 113 | 114 | self.join(tempRoom); 115 | }, 116 | onopen: function (userid, _channel) { 117 | self.onopen(userid, _channel); 118 | self.channels[userid] = { 119 | channel: _channel, 120 | send: function (data) { 121 | self.send(data, this.channel); 122 | } 123 | }; 124 | }, 125 | onmessage: function (data, userid) { 126 | if (IsDataChannelSupported && !data.size) data = JSON.parse(data); 127 | 128 | if (!IsDataChannelSupported) { 129 | if (data.userid === window.userid) return; 130 | data = data.message; 131 | } 132 | 133 | if (data.type === 'text') 134 | textReceiver.receive(data, self.onmessage, userid); 135 | 136 | else if (typeof data.maxChunks != 'undefined') 137 | fileReceiver.receive(data, self); 138 | 139 | else self.onmessage(data, userid); 140 | }, 141 | onclose: function (event) { 142 | var myChannels = self.channels, 143 | closedChannel = event.currentTarget; 144 | 145 | for (var userid in myChannels) { 146 | if (closedChannel === myChannels[userid].channel) { 147 | delete myChannels[userid]; 148 | } 149 | } 150 | 151 | self.onclose(event); 152 | } 153 | }; 154 | 155 | dataConnector = IsDataChannelSupported ? 156 | new DataConnector(self, self.config) : 157 | new SocketConnector(self.channel, self.config); 158 | 159 | fileReceiver = new FileReceiver(self); 160 | textReceiver = new TextReceiver(self); 161 | 162 | if (self.room) self.config.ondatachannel(self.room); 163 | } 164 | 165 | this.open = function (_channel) { 166 | self.joinedARoom = true; 167 | 168 | if (self.socket) self.socket.onDisconnect().remove(); 169 | else self.isInitiator = true; 170 | 171 | if (_channel) self.channel = _channel; 172 | 173 | prepareInit(function () { 174 | init(); 175 | if (IsDataChannelSupported) dataConnector.createRoom(_channel); 176 | }); 177 | }; 178 | 179 | this.connect = function (_channel) { 180 | if (_channel) self.channel = _channel; 181 | prepareInit(init); 182 | }; 183 | 184 | // manually join a room 185 | this.join = function (room) { 186 | if (!room.id || !room.owner) { 187 | throw 'Invalid room info passed.'; 188 | } 189 | 190 | dataConnector.joinRoom({ 191 | roomToken: room.id, 192 | joinUser: room.owner 193 | }); 194 | }; 195 | 196 | this.send = function (data, _channel) { 197 | if (!data) throw 'No file, data or text message to share.'; 198 | if (typeof data.size != 'undefined' && typeof data.type != 'undefined') { 199 | FileSender.send({ 200 | file: data, 201 | channel: dataConnector, 202 | onFileSent: function (file) { 203 | self.onFileSent(file); 204 | }, 205 | onFileProgress: function (packets, uuid) { 206 | self.onFileProgress(packets, uuid); 207 | }, 208 | 209 | _channel: _channel, 210 | root: self 211 | }); 212 | } else { 213 | TextSender.send({ 214 | text: data, 215 | channel: dataConnector, 216 | _channel: _channel, 217 | root: self 218 | }); 219 | } 220 | }; 221 | 222 | this.onleave = function (userid) { 223 | console.debug(userid, 'left!'); 224 | }; 225 | 226 | this.leave = this.eject = function (userid) { 227 | dataConnector.leave(userid, self.autoCloseEntireSession); 228 | }; 229 | 230 | this.openNewSession = function (isOpenNewSession, isNonFirebaseClient) { 231 | if (isOpenNewSession) { 232 | if (self.isNewSessionOpened) return; 233 | self.isNewSessionOpened = true; 234 | 235 | if (!self.joinedARoom) self.open(); 236 | } 237 | 238 | if (!isOpenNewSession || isNonFirebaseClient) self.connect(); 239 | 240 | // for non-firebase clients 241 | if (isNonFirebaseClient) 242 | setTimeout(function () { 243 | self.openNewSession(true); 244 | }, 5000); 245 | }; 246 | 247 | if (typeof this.preferSCTP == 'undefined') { 248 | this.preferSCTP = isFirefox || chromeVersion >= 32 ? true : false; 249 | } 250 | 251 | if (typeof this.chunkSize == 'undefined') { 252 | this.chunkSize = isFirefox || chromeVersion >= 32 ? 13 * 1000 : 1000; // 1000 chars for RTP and 13000 chars for SCTP 253 | } 254 | 255 | if (typeof this.chunkInterval == 'undefined') { 256 | this.chunkInterval = isFirefox || chromeVersion >= 32 ? 100 : 500; // 500ms for RTP and 100ms for SCTP 257 | } 258 | 259 | if (self.automatic) { 260 | if (window.Firebase) { 261 | console.debug('checking presence of the room..'); 262 | new window.Firebase('https://' + (extras.firebase || self.firebase || 'chat') + '.firebaseIO.com/' + self.channel).once('value', function (data) { 263 | console.debug('room is present?', data.val() != null); 264 | self.openNewSession(data.val() == null); 265 | }); 266 | } else self.openNewSession(false, true); 267 | } 268 | }; 269 | 270 | function DataConnector(root, config) { 271 | var self = {}; 272 | var that = this; 273 | 274 | self.userToken = root.userid = root.userid || uniqueToken(); 275 | self.sockets = []; 276 | self.socketObjects = {}; 277 | 278 | var channels = '--', 279 | isbroadcaster, isGetNewRoom = true, 280 | RTCDataChannels = []; 281 | 282 | function newPrivateSocket(_config) { 283 | var socketConfig = { 284 | channel: _config.channel, 285 | onmessage: socketResponse, 286 | onopen: function () { 287 | if (isofferer && !peer) initPeer(); 288 | 289 | _config.socketIndex = socket.index = self.sockets.length; 290 | self.socketObjects[socketConfig.channel] = socket; 291 | self.sockets[_config.socketIndex] = socket; 292 | } 293 | }; 294 | 295 | socketConfig.callback = function (_socket) { 296 | socket = _socket; 297 | socketConfig.onopen(); 298 | }; 299 | 300 | var socket = root.openSignalingChannel(socketConfig), 301 | isofferer = _config.isofferer, 302 | gotstream, inner = {}, peer; 303 | 304 | var peerConfig = { 305 | onICE: function (candidate) { 306 | socket && socket.send({ 307 | userToken: self.userToken, 308 | candidate: { 309 | sdpMLineIndex: candidate.sdpMLineIndex, 310 | candidate: JSON.stringify(candidate.candidate) 311 | } 312 | }); 313 | }, 314 | onopen: onChannelOpened, 315 | onmessage: function (event) { 316 | config.onmessage(event.data, _config.userid); 317 | }, 318 | onclose: config.onclose, 319 | onerror: root.onerror, 320 | preferSCTP: root.preferSCTP 321 | }; 322 | 323 | function initPeer(offerSDP) { 324 | if (root.direction === 'one-to-one' && window.isFirstConnectionOpened) return; 325 | 326 | if (!offerSDP) peerConfig.onOfferSDP = sendsdp; 327 | else { 328 | peerConfig.offerSDP = offerSDP; 329 | peerConfig.onAnswerSDP = sendsdp; 330 | } 331 | 332 | peer = RTCPeerConnection(peerConfig); 333 | } 334 | 335 | function onChannelOpened(channel) { 336 | channel.peer = peer.peer; 337 | RTCDataChannels.push(channel); 338 | 339 | config.onopen(_config.userid, channel); 340 | 341 | if (root.direction === 'many-to-many' && isbroadcaster && channels.split('--').length > 3) { 342 | defaultSocket && defaultSocket.send({ 343 | newParticipant: socket.channel, 344 | userToken: self.userToken 345 | }); 346 | } 347 | 348 | window.isFirstConnectionOpened = gotstream = true; 349 | } 350 | 351 | function sendsdp(sdp) { 352 | sdp = JSON.stringify(sdp); 353 | var part = parseInt(sdp.length / 3); 354 | 355 | var firstPart = sdp.slice(0, part), 356 | secondPart = sdp.slice(part, sdp.length - 1), 357 | thirdPart = ''; 358 | 359 | if (sdp.length > part + part) { 360 | secondPart = sdp.slice(part, part + part); 361 | thirdPart = sdp.slice(part + part, sdp.length); 362 | } 363 | 364 | socket.send({ 365 | userToken: self.userToken, 366 | firstPart: firstPart 367 | }); 368 | 369 | socket.send({ 370 | userToken: self.userToken, 371 | secondPart: secondPart 372 | }); 373 | 374 | socket.send({ 375 | userToken: self.userToken, 376 | thirdPart: thirdPart 377 | }); 378 | } 379 | 380 | function socketResponse(response) { 381 | if (response.userToken == self.userToken) return; 382 | 383 | if (response.firstPart || response.secondPart || response.thirdPart) { 384 | if (response.firstPart) { 385 | // sdp sender's user id passed over "onopen" method 386 | _config.userid = response.userToken; 387 | 388 | inner.firstPart = response.firstPart; 389 | if (inner.secondPart && inner.thirdPart) selfInvoker(); 390 | } 391 | if (response.secondPart) { 392 | inner.secondPart = response.secondPart; 393 | if (inner.firstPart && inner.thirdPart) selfInvoker(); 394 | } 395 | 396 | if (response.thirdPart) { 397 | inner.thirdPart = response.thirdPart; 398 | if (inner.firstPart && inner.secondPart) selfInvoker(); 399 | } 400 | } 401 | 402 | if (response.candidate && !gotstream) { 403 | peer && peer.addICE({ 404 | sdpMLineIndex: response.candidate.sdpMLineIndex, 405 | candidate: JSON.parse(response.candidate.candidate) 406 | }); 407 | 408 | console.debug('ice candidate', response.candidate.candidate); 409 | } 410 | 411 | if (response.left) { 412 | if (peer && peer.peer) { 413 | peer.peer.close(); 414 | peer.peer = null; 415 | } 416 | 417 | if (response.closeEntireSession) leaveChannels(); 418 | else if (socket) { 419 | socket.send({ 420 | left: true, 421 | userToken: self.userToken 422 | }); 423 | socket = null; 424 | } 425 | 426 | root.onleave(response.userToken); 427 | } 428 | 429 | if (response.playRoleOfBroadcaster) 430 | setTimeout(function () { 431 | self.roomToken = response.roomToken; 432 | root.open(self.roomToken); 433 | self.sockets = swap(self.sockets); 434 | }, 600); 435 | } 436 | 437 | var invokedOnce = false; 438 | 439 | function selfInvoker() { 440 | if (invokedOnce) return; 441 | 442 | invokedOnce = true; 443 | inner.sdp = JSON.parse(inner.firstPart + inner.secondPart + inner.thirdPart); 444 | 445 | if (isofferer) peer.addAnswerSDP(inner.sdp); 446 | else initPeer(inner.sdp); 447 | 448 | console.debug('sdp', inner.sdp.sdp); 449 | } 450 | } 451 | 452 | function onNewParticipant(channel) { 453 | if (!channel || channels.indexOf(channel) != -1 || channel == self.userToken) return; 454 | channels += channel + '--'; 455 | 456 | var new_channel = uniqueToken(); 457 | 458 | newPrivateSocket({ 459 | channel: new_channel, 460 | closeSocket: true 461 | }); 462 | 463 | defaultSocket && defaultSocket.send({ 464 | participant: true, 465 | userToken: self.userToken, 466 | joinUser: channel, 467 | channel: new_channel 468 | }); 469 | } 470 | 471 | function uniqueToken() { 472 | return Math.round(Math.random() * 60535) + 5000000; 473 | } 474 | 475 | function leaveChannels(channel) { 476 | var alert = { 477 | left: true, 478 | userToken: self.userToken 479 | }; 480 | 481 | // if room initiator is leaving the room; close the entire session 482 | if (isbroadcaster) { 483 | if (root.autoCloseEntireSession) alert.closeEntireSession = true; 484 | else 485 | self.sockets[0].send({ 486 | playRoleOfBroadcaster: true, 487 | userToken: self.userToken, 488 | roomToken: self.roomToken 489 | }); 490 | } 491 | 492 | if (!channel) { 493 | // closing all sockets 494 | var sockets = self.sockets, 495 | length = sockets.length; 496 | 497 | for (var i = 0; i < length; i++) { 498 | var socket = sockets[i]; 499 | if (socket) { 500 | socket.send(alert); 501 | 502 | if (self.socketObjects[socket.channel]) 503 | delete self.socketObjects[socket.channel]; 504 | 505 | delete sockets[i]; 506 | } 507 | } 508 | 509 | that.left = true; 510 | } 511 | 512 | // eject a specific user! 513 | if (channel) { 514 | socket = self.socketObjects[channel]; 515 | if (socket) { 516 | socket.send(alert); 517 | 518 | if (self.sockets[socket.index]) 519 | delete self.sockets[socket.index]; 520 | 521 | delete self.socketObjects[channel]; 522 | } 523 | } 524 | self.sockets = swap(self.sockets); 525 | } 526 | 527 | window.addEventListener('beforeunload', function () { 528 | leaveChannels(); 529 | }, false); 530 | 531 | window.addEventListener('keydown', function (e) { 532 | if (e.keyCode == 116) 533 | leaveChannels(); 534 | }, false); 535 | 536 | var defaultSocket = root.openSignalingChannel({ 537 | onmessage: function (response) { 538 | if (response.userToken == self.userToken) return; 539 | if (isGetNewRoom && response.roomToken && response.broadcaster) config.ondatachannel(response); 540 | 541 | if (response.newParticipant) onNewParticipant(response.newParticipant); 542 | 543 | if (response.userToken && response.joinUser == self.userToken && response.participant && channels.indexOf(response.userToken) == -1) { 544 | channels += response.userToken + '--'; 545 | 546 | console.debug('Data connection is being opened between you and', response.userToken || response.channel); 547 | newPrivateSocket({ 548 | isofferer: true, 549 | channel: response.channel || response.userToken, 550 | closeSocket: true 551 | }); 552 | } 553 | }, 554 | callback: function (socket) { 555 | defaultSocket = socket; 556 | } 557 | }); 558 | 559 | return { 560 | createRoom: function (roomToken) { 561 | self.roomToken = roomToken || uniqueToken(); 562 | 563 | isbroadcaster = true; 564 | isGetNewRoom = false; 565 | 566 | (function transmit() { 567 | defaultSocket && defaultSocket.send({ 568 | roomToken: self.roomToken, 569 | broadcaster: self.userToken 570 | }); 571 | 572 | if (!root.transmitRoomOnce && !that.leaving) { 573 | if (root.direction === 'one-to-one') { 574 | if (!window.isFirstConnectionOpened) setTimeout(transmit, 3000); 575 | } else setTimeout(transmit, 3000); 576 | } 577 | })(); 578 | }, 579 | joinRoom: function (_config) { 580 | self.roomToken = _config.roomToken; 581 | isGetNewRoom = false; 582 | 583 | newPrivateSocket({ 584 | channel: self.userToken 585 | }); 586 | 587 | defaultSocket.send({ 588 | participant: true, 589 | userToken: self.userToken, 590 | joinUser: _config.joinUser 591 | }); 592 | }, 593 | send: function (message, _channel) { 594 | var _channels = RTCDataChannels, 595 | data, length = _channels.length; 596 | if (!length) return; 597 | 598 | data = JSON.stringify(message); 599 | 600 | if (_channel) { 601 | if (_channel.readyState == 'open') { 602 | _channel.send(data); 603 | } 604 | } 605 | else 606 | for (var i = 0; i < length; i++) { 607 | if (_channels[i].readyState == 'open') { 608 | _channels[i].send(data); 609 | }; 610 | } 611 | }, 612 | leave: function (userid, autoCloseEntireSession) { 613 | if (autoCloseEntireSession) root.autoCloseEntireSession = true; 614 | leaveChannels(userid); 615 | if (!userid) { 616 | self.joinedARoom = isbroadcaster = false; 617 | isGetNewRoom = true; 618 | } 619 | } 620 | }; 621 | } 622 | 623 | function SocketConnector(_channel, config) { 624 | var channel = config.openSocket({ 625 | channel: _channel, 626 | onopen: config.onopen, 627 | onmessage: config.onmessage 628 | }); 629 | 630 | return { 631 | send: function (message) { 632 | channel && channel.send({ 633 | userid: userid, 634 | message: message 635 | }); 636 | } 637 | }; 638 | } 639 | 640 | function getRandomString() { 641 | return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, '-'); 642 | } 643 | 644 | window.userid = getRandomString(); 645 | 646 | var isMobileDevice = navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i); 647 | var isChrome = !!navigator.webkitGetUserMedia; 648 | var isFirefox = !!navigator.mozGetUserMedia; 649 | var chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]); 650 | 651 | var FileSender = { 652 | send: function (config) { 653 | var root = config.root; 654 | var channel = config.channel; 655 | var privateChannel = config._channel; 656 | var file = config.file; 657 | 658 | if (!config.file) { 659 | console.error('You must attach/select a file.'); 660 | return; 661 | } 662 | 663 | // max chunk sending limit on chrome is 64k 664 | // max chunk receiving limit on firefox is 16k 665 | var packetSize = (!!navigator.mozGetUserMedia || root.preferSCTP) ? 15 * 1000 : 1 * 1000; 666 | 667 | if (root.chunkSize) { 668 | packetSize = root.chunkSize; 669 | } 670 | 671 | var textToTransfer = ''; 672 | var numberOfPackets = 0; 673 | var packets = 0; 674 | 675 | file.uuid = getRandomString(); 676 | 677 | function processInWebWorker() { 678 | var blob = URL.createObjectURL(new Blob(['function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}'], { 679 | type: 'application/javascript' 680 | })); 681 | 682 | var worker = new Worker(blob); 683 | URL.revokeObjectURL(blob); 684 | return worker; 685 | } 686 | 687 | if (!!window.Worker && !isMobileDevice) { 688 | var webWorker = processInWebWorker(); 689 | 690 | webWorker.onmessage = function (event) { 691 | onReadAsDataURL(event.data); 692 | }; 693 | 694 | webWorker.postMessage(file); 695 | } else { 696 | var reader = new FileReader(); 697 | reader.onload = function (e) { 698 | onReadAsDataURL(e.target.result); 699 | }; 700 | reader.readAsDataURL(file); 701 | } 702 | 703 | function onReadAsDataURL(dataURL, text) { 704 | var data = { 705 | type: 'file', 706 | uuid: file.uuid, 707 | maxChunks: numberOfPackets, 708 | currentPosition: numberOfPackets - packets, 709 | name: file.name, 710 | fileType: file.type, 711 | size: file.size 712 | }; 713 | 714 | if (dataURL) { 715 | text = dataURL; 716 | numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); 717 | 718 | file.maxChunks = data.maxChunks = numberOfPackets; 719 | data.currentPosition = numberOfPackets - packets; 720 | 721 | if(root.onFileSent) root.onFileSent(file); 722 | } 723 | 724 | if(root.onFileProgress) root.onFileProgress({ 725 | remaining: packets--, 726 | length: numberOfPackets, 727 | sent: numberOfPackets - packets, 728 | 729 | maxChunks: numberOfPackets, 730 | uuid: file.uuid, 731 | currentPosition: numberOfPackets - packets 732 | }, file.uuid); 733 | 734 | if (text.length > packetSize) data.message = text.slice(0, packetSize); 735 | else { 736 | data.message = text; 737 | data.last = true; 738 | data.name = file.name; 739 | 740 | file.url = URL.createObjectURL(file); 741 | root.onFileSent(file, file.uuid); 742 | } 743 | 744 | channel.send(data, privateChannel); 745 | 746 | textToTransfer = text.slice(data.message.length); 747 | if (textToTransfer.length) { 748 | setTimeout(function () { 749 | onReadAsDataURL(null, textToTransfer); 750 | }, root.chunkInterval || 100); 751 | } 752 | } 753 | } 754 | }; 755 | 756 | function FileReceiver(root) { 757 | var content = {}, 758 | packets = {}, 759 | numberOfPackets = {}; 760 | 761 | function receive(data) { 762 | var uuid = data.uuid; 763 | 764 | if (typeof data.packets !== 'undefined') { 765 | numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); 766 | } 767 | 768 | if(root.onFileProgress) root.onFileProgress({ 769 | remaining: packets[uuid]--, 770 | length: numberOfPackets[uuid], 771 | received: numberOfPackets[uuid] - packets[uuid], 772 | 773 | maxChunks: numberOfPackets[uuid], 774 | uuid: uuid, 775 | currentPosition: numberOfPackets[uuid] - packets[uuid] 776 | }, uuid); 777 | 778 | if (!content[uuid]) content[uuid] = []; 779 | 780 | content[uuid].push(data.message); 781 | 782 | if (data.last) { 783 | var dataURL = content[uuid].join(''); 784 | 785 | FileConverter.DataURLToBlob(dataURL, data.fileType, function (blob) { 786 | blob.uuid = uuid; 787 | blob.name = data.name; 788 | blob.type = data.fileType; 789 | blob.extra = data.extra || {}; 790 | 791 | blob.url = (window.URL || window.webkitURL).createObjectURL(blob); 792 | 793 | if (root.autoSaveToDisk) { 794 | FileSaver.SaveToDisk(blob.url, data.name); 795 | } 796 | 797 | if(root.onFileReceived) root.onFileReceived(blob); 798 | 799 | delete content[uuid]; 800 | }); 801 | } 802 | } 803 | 804 | return { 805 | receive: receive 806 | }; 807 | } 808 | 809 | var FileSaver = { 810 | SaveToDisk: function (fileUrl, fileName) { 811 | var hyperlink = document.createElement('a'); 812 | hyperlink.href = fileUrl; 813 | hyperlink.target = '_blank'; 814 | hyperlink.download = fileName || fileUrl; 815 | 816 | var mouseEvent = new MouseEvent('click', { 817 | view: window, 818 | bubbles: true, 819 | cancelable: true 820 | }); 821 | 822 | hyperlink.dispatchEvent(mouseEvent); 823 | (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); 824 | } 825 | }; 826 | 827 | var FileConverter = { 828 | DataURLToBlob: function (dataURL, fileType, callback) { 829 | 830 | function processInWebWorker() { 831 | var blob = URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'], { 832 | type: 'application/javascript' 833 | })); 834 | 835 | var worker = new Worker(blob); 836 | URL.revokeObjectURL(blob); 837 | return worker; 838 | } 839 | 840 | if (!!window.Worker && !isMobileDevice) { 841 | var webWorker = processInWebWorker(); 842 | 843 | webWorker.onmessage = function (event) { 844 | callback(event.data); 845 | }; 846 | 847 | webWorker.postMessage(JSON.stringify({ 848 | dataURL: dataURL, 849 | fileType: fileType 850 | })); 851 | } else { 852 | var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)), 853 | i = binary.length, 854 | view = new Uint8Array(i); 855 | 856 | while (i--) { 857 | view[i] = binary.charCodeAt(i); 858 | } 859 | 860 | callback(new Blob([view])); 861 | } 862 | } 863 | }; 864 | 865 | var TextSender = { 866 | send: function (config) { 867 | var root = config.root; 868 | 869 | var channel = config.channel, 870 | _channel = config._channel, 871 | initialText = config.text, 872 | packetSize = root.chunkSize || 1000, 873 | textToTransfer = '', 874 | isobject = false; 875 | 876 | if (typeof initialText !== 'string') { 877 | isobject = true; 878 | initialText = JSON.stringify(initialText); 879 | } 880 | 881 | // uuid is used to uniquely identify sending instance 882 | var uuid = getRandomString(); 883 | var sendingTime = new Date().getTime(); 884 | 885 | sendText(initialText); 886 | 887 | function sendText(textMessage, text) { 888 | var data = { 889 | type: 'text', 890 | uuid: uuid, 891 | sendingTime: sendingTime 892 | }; 893 | 894 | if (textMessage) { 895 | text = textMessage; 896 | data.packets = parseInt(text.length / packetSize); 897 | } 898 | 899 | if (text.length > packetSize) 900 | data.message = text.slice(0, packetSize); 901 | else { 902 | data.message = text; 903 | data.last = true; 904 | data.isobject = isobject; 905 | } 906 | 907 | channel.send(data, _channel); 908 | 909 | textToTransfer = text.slice(data.message.length); 910 | 911 | if (textToTransfer.length) { 912 | setTimeout(function () { 913 | sendText(null, textToTransfer); 914 | }, root.chunkInterval || 100); 915 | } 916 | } 917 | } 918 | }; 919 | 920 | // _______________ 921 | // TextReceiver.js 922 | 923 | function TextReceiver() { 924 | var content = {}; 925 | 926 | function receive(data, onmessage, userid) { 927 | // uuid is used to uniquely identify sending instance 928 | var uuid = data.uuid; 929 | if (!content[uuid]) content[uuid] = []; 930 | 931 | content[uuid].push(data.message); 932 | if (data.last) { 933 | var message = content[uuid].join(''); 934 | if (data.isobject) message = JSON.parse(message); 935 | 936 | // latency detection 937 | var receivingTime = new Date().getTime(); 938 | var latency = receivingTime - data.sendingTime; 939 | 940 | onmessage(message, userid, latency); 941 | 942 | delete content[uuid]; 943 | } 944 | } 945 | 946 | return { 947 | receive: receive 948 | }; 949 | } 950 | 951 | function swap(arr) { 952 | var swapped = [], 953 | length = arr.length; 954 | for (var i = 0; i < length; i++) 955 | if (arr[i]) swapped.push(arr[i]); 956 | return swapped; 957 | } 958 | 959 | window.moz = !!navigator.mozGetUserMedia; 960 | window.IsDataChannelSupported = !((moz && !navigator.mozGetUserMedia) || (!moz && !navigator.webkitGetUserMedia)); 961 | 962 | function RTCPeerConnection(options) { 963 | var w = window, 964 | PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, 965 | SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, 966 | IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; 967 | 968 | var iceServers = []; 969 | 970 | if (isFirefox) { 971 | iceServers.push({ 972 | url: 'stun:23.21.150.121' 973 | }); 974 | 975 | iceServers.push({ 976 | url: 'stun:stun.services.mozilla.com' 977 | }); 978 | } 979 | 980 | if (isChrome) { 981 | iceServers.push({ 982 | url: 'stun:stun.l.google.com:19302' 983 | }); 984 | 985 | iceServers.push({ 986 | url: 'stun:stun.anyfirewall.com:3478' 987 | }); 988 | } 989 | 990 | if (isChrome && chromeVersion < 28) { 991 | iceServers.push({ 992 | url: 'turn:homeo@turn.bistri.com:80?transport=udp', 993 | credential: 'homeo' 994 | }); 995 | 996 | iceServers.push({ 997 | url: 'turn:homeo@turn.bistri.com:80?transport=tcp', 998 | credential: 'homeo' 999 | }); 1000 | } 1001 | 1002 | if (isChrome && chromeVersion >= 28) { 1003 | iceServers.push({ 1004 | url: 'turn:turn.bistri.com:80?transport=udp', 1005 | credential: 'homeo', 1006 | username: 'homeo' 1007 | }); 1008 | 1009 | iceServers.push({ 1010 | url: 'turn:turn.bistri.com:80?transport=tcp', 1011 | credential: 'homeo', 1012 | username: 'homeo' 1013 | }); 1014 | 1015 | iceServers.push({ 1016 | url: 'turn:turn.anyfirewall.com:443?transport=tcp', 1017 | credential: 'webrtc', 1018 | username: 'webrtc' 1019 | }); 1020 | } 1021 | 1022 | if (options.iceServers) iceServers = options.iceServers; 1023 | 1024 | iceServers = { 1025 | iceServers: iceServers 1026 | }; 1027 | 1028 | var optional = { 1029 | optional: [] 1030 | }; 1031 | 1032 | if (!moz && !options.preferSCTP) { 1033 | optional.optional = [{ 1034 | RtpDataChannels: true 1035 | }]; 1036 | } 1037 | 1038 | if (!navigator.onLine) { 1039 | iceServers = null; 1040 | console.warn('No internet connection detected. No STUN/TURN server is used to make sure local/host candidates are used for peers connection.'); 1041 | } 1042 | 1043 | var peerConnection = new PeerConnection(iceServers, optional); 1044 | 1045 | openOffererChannel(); 1046 | peerConnection.onicecandidate = onicecandidate; 1047 | 1048 | function onicecandidate(event) { 1049 | if (!event.candidate || !peerConnection) return; 1050 | if (options.onICE) options.onICE(event.candidate); 1051 | } 1052 | 1053 | var constraints = options.constraints || { 1054 | optional: [], 1055 | mandatory: { 1056 | OfferToReceiveAudio: !!moz, 1057 | OfferToReceiveVideo: !!moz 1058 | } 1059 | }; 1060 | 1061 | function onSdpError(e) { 1062 | var message = JSON.stringify(e, null, '\t'); 1063 | 1064 | if (message.indexOf('RTP/SAVPF Expects at least 4 fields') != -1) { 1065 | message = 'It seems that you are trying to interop RTP-datachannels with SCTP. It is not supported!'; 1066 | } 1067 | 1068 | console.error('onSdpError:', message); 1069 | } 1070 | 1071 | function onSdpSuccess() { 1072 | } 1073 | 1074 | function createOffer() { 1075 | if (!options.onOfferSDP) return; 1076 | 1077 | peerConnection.createOffer(function (sessionDescription) { 1078 | sessionDescription.sdp = setBandwidth(sessionDescription.sdp); 1079 | peerConnection.setLocalDescription(sessionDescription); 1080 | options.onOfferSDP(sessionDescription); 1081 | }, onSdpError, constraints); 1082 | } 1083 | 1084 | function createAnswer() { 1085 | if (!options.onAnswerSDP) return; 1086 | 1087 | options.offerSDP = new SessionDescription(options.offerSDP); 1088 | peerConnection.setRemoteDescription(options.offerSDP, onSdpSuccess, onSdpError); 1089 | 1090 | peerConnection.createAnswer(function (sessionDescription) { 1091 | sessionDescription.sdp = setBandwidth(sessionDescription.sdp); 1092 | peerConnection.setLocalDescription(sessionDescription); 1093 | options.onAnswerSDP(sessionDescription); 1094 | }, onSdpError, constraints); 1095 | } 1096 | 1097 | function setBandwidth(sdp) { 1098 | // Firefox has no support of "b=AS" 1099 | if (moz) return sdp; 1100 | 1101 | // remove existing bandwidth lines 1102 | sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); 1103 | sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:1638400\r\n'); 1104 | 1105 | return sdp; 1106 | } 1107 | 1108 | if (!moz) { 1109 | createOffer(); 1110 | createAnswer(); 1111 | } 1112 | 1113 | var channel; 1114 | 1115 | function openOffererChannel() { 1116 | if (moz && !options.onOfferSDP) return; 1117 | 1118 | if(!moz && options.preferSCTP && !options.onOfferSDP) return; 1119 | 1120 | _openOffererChannel(); 1121 | if (moz) { 1122 | navigator.mozGetUserMedia({ 1123 | audio: true, 1124 | fake: true 1125 | }, function (stream) { 1126 | peerConnection.addStream(stream); 1127 | createOffer(); 1128 | }, useless); 1129 | } 1130 | } 1131 | 1132 | function _openOffererChannel() { 1133 | // protocol: 'text/chat', preset: true, stream: 16 1134 | // maxRetransmits:0 && ordered:false 1135 | var dataChannelDict = {}; 1136 | 1137 | if (!moz && !options.preferSCTP) { 1138 | dataChannelDict.reliable = false; // Deprecated! 1139 | } 1140 | 1141 | console.debug('dataChannelDict', dataChannelDict); 1142 | channel = peerConnection.createDataChannel('channel', dataChannelDict); 1143 | setChannelEvents(); 1144 | } 1145 | 1146 | function setChannelEvents() { 1147 | channel.onmessage = options.onmessage; 1148 | channel.onopen = function () { 1149 | options.onopen(channel); 1150 | }; 1151 | channel.onclose = options.onclose; 1152 | channel.onerror = options.onerror; 1153 | } 1154 | 1155 | if (options.onAnswerSDP && moz && options.onmessage) openAnswererChannel(); 1156 | if(!moz && options.preferSCTP && !options.onOfferSDP) openAnswererChannel(); 1157 | 1158 | function openAnswererChannel() { 1159 | peerConnection.ondatachannel = function (event) { 1160 | channel = event.channel; 1161 | setChannelEvents(); 1162 | }; 1163 | 1164 | if (moz) { 1165 | navigator.mozGetUserMedia({ 1166 | audio: true, 1167 | fake: true 1168 | }, function (stream) { 1169 | peerConnection.addStream(stream); 1170 | createAnswer(); 1171 | }, useless); 1172 | } 1173 | } 1174 | 1175 | function useless() { 1176 | } 1177 | 1178 | return { 1179 | addAnswerSDP: function (sdp) { 1180 | sdp = new SessionDescription(sdp); 1181 | peerConnection.setRemoteDescription(sdp, onSdpSuccess, onSdpError); 1182 | }, 1183 | addICE: function (candidate) { 1184 | peerConnection.addIceCandidate(new IceCandidate({ 1185 | sdpMLineIndex: candidate.sdpMLineIndex, 1186 | candidate: candidate.candidate 1187 | })); 1188 | }, 1189 | 1190 | peer: peerConnection, 1191 | channel: channel, 1192 | sendData: function (message) { 1193 | channel && channel.send(message); 1194 | } 1195 | }; 1196 | } 1197 | })(); 1198 | --------------------------------------------------------------------------------