├── AndroidRTC ├── images │ ├── logo.png │ ├── camera.png │ ├── loader.gif │ ├── settings.png │ ├── microphone.png │ ├── search-user.png │ ├── send-message.png │ └── share-files.png ├── fonts │ ├── MyriadPro-Bold.otf │ ├── MyriadPro-Light.otf │ └── MyriadPro-Regular.otf ├── scripts │ ├── common-signaling.js │ ├── ui-handler.js │ ├── conversation.js │ └── FileBufferReader.js ├── manifest.json ├── styles │ └── ui-styles.css └── index.html ├── demos ├── common-signaling.js ├── common-styles.css ├── search-user.html └── cross-language-chat.html ├── package.json ├── server.js ├── README.md └── conversation.js /AndroidRTC/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/images/logo.png -------------------------------------------------------------------------------- /AndroidRTC/images/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/images/camera.png -------------------------------------------------------------------------------- /AndroidRTC/images/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/images/loader.gif -------------------------------------------------------------------------------- /AndroidRTC/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/images/settings.png -------------------------------------------------------------------------------- /AndroidRTC/images/microphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/images/microphone.png -------------------------------------------------------------------------------- /AndroidRTC/fonts/MyriadPro-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/fonts/MyriadPro-Bold.otf -------------------------------------------------------------------------------- /AndroidRTC/images/search-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/images/search-user.png -------------------------------------------------------------------------------- /AndroidRTC/images/send-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/images/send-message.png -------------------------------------------------------------------------------- /AndroidRTC/images/share-files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/images/share-files.png -------------------------------------------------------------------------------- /AndroidRTC/fonts/MyriadPro-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/fonts/MyriadPro-Light.otf -------------------------------------------------------------------------------- /AndroidRTC/fonts/MyriadPro-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/Conversation.js/master/AndroidRTC/fonts/MyriadPro-Regular.otf -------------------------------------------------------------------------------- /demos/common-signaling.js: -------------------------------------------------------------------------------- 1 | var SIGNALING_SERVER = 'wss://webrtcweb.com:9449/'; 2 | var channel = window.RMCDefaultChannel; 3 | 4 | var websocket = new WebSocket(SIGNALING_SERVER); 5 | 6 | websocket.push = websocket.send; 7 | websocket.send = function(data) { 8 | websocket.push(JSON.stringify({ 9 | channel: channel, 10 | data: data 11 | })); 12 | }; 13 | 14 | var signaler = new Signaler(); 15 | signaler.on('message', function(data) { 16 | websocket.send(data); 17 | }); 18 | 19 | websocket.onmessage = function(event) { 20 | var data = JSON.parse(event.data); 21 | signaler.emit('message', data); 22 | }; 23 | 24 | websocket.onopen = function(){ 25 | console.info('WebSocket connection is opened'); 26 | 27 | websocket.send({ 28 | channel: channel 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /AndroidRTC/scripts/common-signaling.js: -------------------------------------------------------------------------------- 1 | var SIGNALING_SERVER = 'wss://webrtcweb.com:9449/'; 2 | var channel = window.RMCDefaultChannel; 3 | 4 | var websocket = new WebSocket(SIGNALING_SERVER); 5 | 6 | websocket.push = websocket.send; 7 | websocket.send = function(data) { 8 | websocket.push(JSON.stringify({ 9 | channel: channel, 10 | data: data 11 | })); 12 | }; 13 | 14 | var signaler = new Signaler(); 15 | signaler.on('message', function(data) { 16 | websocket.send(data); 17 | }); 18 | 19 | websocket.onmessage = function(event) { 20 | var data = JSON.parse(event.data); 21 | signaler.emit('message', data); 22 | }; 23 | 24 | websocket.onopen = function(){ 25 | console.info('WebSocket connection is opened'); 26 | 27 | websocket.send({ 28 | channel: channel 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /AndroidRTC/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AndroidRTC", 3 | "description": "AndroidRTC is using WebRTC standards to bring realtime [secure] p2p connections!", 4 | "version": "0.0.0.3", 5 | "app": { 6 | "main": { 7 | "scripts": [ 8 | "scripts/socket.io.js", 9 | "scripts/RTCMultiConnection.js", 10 | "scripts/FileBufferReader.js", 11 | "scripts/conversation.js", 12 | "scripts/common-signaling.js", 13 | "scripts/ui-handler.js", 14 | "styles/ui-styles.css", 15 | "fonts/MyriadPro-Bold.otf", 16 | "fonts/MyriadPro-Light.otf", 17 | "fonts/MyriadPro-Regular.otf", 18 | "images/camera.png", 19 | "images/loader.gif", 20 | "images/logo.png", 21 | "images/microphone.png", 22 | "images/search-user.png", 23 | "images/send-message.png", 24 | "images/settings.png", 25 | "images/share-files.png" 26 | ] 27 | }, 28 | "launch":{ 29 | "local_path": "index.html" 30 | } 31 | }, 32 | "icons": { 33 | "128": "images/logo.png" 34 | } 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conversationjs", 3 | "preferGlobal": true, 4 | "version": "1.0.0", 5 | "author": { 6 | "name": "Muaz Khan", 7 | "email": "muazkh@gmail.com", 8 | "url": "http://www.muazkhan.com/" 9 | }, 10 | "description": "Conversation.js is inspired by skype; and it provides simple events-like API to manage conversations, enable/disable media devices; add/download files; and do anything supported by Skype.", 11 | "scripts": { 12 | "start": "node conversation.js" 13 | }, 14 | "main": "./conversation.js", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/muaz-khan/Conversation.git" 18 | }, 19 | "keywords": [ 20 | "webrtc", 21 | "conversation", 22 | "conversation.js", 23 | "conversationjs", 24 | "rtcmulticonnection", 25 | "webrtc-library", 26 | "library", 27 | "javascript", 28 | "chrome", 29 | "firefox", 30 | "opera", 31 | "ie", 32 | "internet-explorer", 33 | "android", 34 | "rtcweb", 35 | "rtcmulticonnection.js", 36 | "multirtc", 37 | "webrtc-experiment", 38 | "javascript-library", 39 | "muaz", 40 | "muaz-khan" 41 | ], 42 | "analyze": false, 43 | "license": "MIT", 44 | "readmeFilename": "README.md", 45 | "bugs": { 46 | "url": "https://github.com/muaz-khan/Conversation/issues", 47 | "email": "muazkh@gmail.com" 48 | }, 49 | "homepage": "https://www.webrtc-experiment.com/Conversationjs/", 50 | "_id": "conversationjs@1.0.0", 51 | "_from": "conversationjs@" 52 | } 53 | -------------------------------------------------------------------------------- /demos/common-styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-user-select: none; 3 | -o-user-select: none; 4 | -ms-user-select: none; 5 | -moz-user-select: none; 6 | user-select: none; 7 | 8 | font-family: Calibri; 9 | font-family: 'Segoe UI Light', 'Segoe UI'; 10 | font-size: 1.1em; 11 | font-weight: normal; 12 | } 13 | 14 | html { 15 | background: #eee; 16 | } 17 | 18 | body { 19 | font-size: 1.2em; 20 | line-height: 1.2em; 21 | margin: 0; 22 | } 23 | 24 | input[type=text], input[type=search] { 25 | background: none repeat scroll 0 0 #F9F9F9; 26 | border: 1px solid #CCCCCC; 27 | border-radius: 0 0 5px 5px; 28 | border-top: 0; 29 | font: 300 18px/40px'light', inherit; 30 | height: 40px; 31 | letter-spacing: 1px; 32 | margin-bottom: 4px; 33 | padding: 5px 10px; 34 | width: 100%; 35 | } 36 | 37 | #output { 38 | background: none repeat scroll 0 0 #F9F9F9; 39 | border: 1px solid #CCCCCC; 40 | border-radius: 5px 5px 0 0; 41 | font: 300 18px/40px'light', 'Helvetica Neue', Arial, Helvetica, sans-serif; 42 | height: 400px; 43 | letter-spacing: 1px; 44 | margin-bottom: 0; 45 | overflow: scroll; 46 | padding: 5px 10px; 47 | } 48 | 49 | #output div { 50 | border-bottom: 1px solid #CCCCCC; 51 | } 52 | 53 | section.name { 54 | float: left; 55 | overflow: hidden; 56 | padding-left: 2em; 57 | padding-right: 1em; 58 | text-align: right; 59 | width: 7em; 60 | height: 35px; 61 | } 62 | 63 | section.message { 64 | border-left: 1px solid #CCCCCC; 65 | margin-left: 10em; 66 | overflow: hidden; 67 | padding-left: 1em; 68 | } 69 | 70 | input, .message { 71 | -webkit-user-select: initial; 72 | -o-user-select: initial; 73 | -ms-user-select: initial; 74 | -moz-user-select: initial; 75 | user-select: initial; 76 | } 77 | 78 | button[disabled], input[disabled] { 79 | background: rgba(216, 205, 205, 0.2); 80 | border: 1px solid rgb(233, 224, 224); 81 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // http://127.0.0.1:9001 2 | // http://localhost:9001 3 | 4 | var server = require('http'), 5 | url = require('url'), 6 | path = require('path'), 7 | fs = require('fs'); 8 | 9 | function serverHandler(request, response) { 10 | var uri = url.parse(request.url).pathname, 11 | filename = path.join(process.cwd(), uri); 12 | 13 | fs.exists(filename, function(exists) { 14 | if (!exists) { 15 | response.writeHead(404, { 16 | 'Content-Type': 'text/plain' 17 | }); 18 | response.write('404 Not Found: ' + filename + '\n'); 19 | response.end(); 20 | return; 21 | } 22 | 23 | if (filename.indexOf('favicon.ico') !== -1) { 24 | return; 25 | } 26 | 27 | var isWin = !!process.platform.match(/^win/); 28 | 29 | if (fs.statSync(filename).isDirectory() && !isWin) { 30 | filename += '/index.html'; 31 | } else if (fs.statSync(filename).isDirectory() && !!isWin) { 32 | filename += '\\index.html'; 33 | } 34 | 35 | fs.readFile(filename, 'binary', function(err, file) { 36 | if (err) { 37 | response.writeHead(500, { 38 | 'Content-Type': 'text/plain' 39 | }); 40 | response.write(err + '\n'); 41 | response.end(); 42 | return; 43 | } 44 | 45 | var contentType; 46 | 47 | if (filename.indexOf('.html') !== -1) { 48 | contentType = 'text/html'; 49 | } 50 | 51 | if (filename.indexOf('.js') !== -1) { 52 | contentType = 'application/javascript'; 53 | } 54 | 55 | if (contentType) { 56 | response.writeHead(200, { 57 | 'Content-Type': contentType 58 | }); 59 | } else response.writeHead(200); 60 | 61 | response.write(file, 'binary'); 62 | response.end(); 63 | }); 64 | }); 65 | } 66 | 67 | var app; 68 | 69 | app = server.createServer(serverHandler); 70 | 71 | app = app.listen(process.env.PORT || 9001, process.env.IP || "0.0.0.0", function() { 72 | var addr = app.address(); 73 | console.log("Server listening at", addr.address + ":" + addr.port); 74 | }); 75 | -------------------------------------------------------------------------------- /AndroidRTC/styles/ui-styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Myriad'; 3 | src: url('../fonts/MyriadPro-Light.otf') format("opentype"); 4 | font-weight:400; 5 | } 6 | 7 | * { 8 | margin:0; 9 | padding: 0; 10 | color: black; 11 | font-family:Myriad, Arial, Verdana; 12 | 13 | -webkit-user-select: none; 14 | -moz-user-select: none; 15 | -o-user-select: none; 16 | -ms-user-select: none; 17 | user-select: none; 18 | 19 | -webkit-user-drag: none; 20 | -moz-user-drag: none; 21 | -o-user-drag: none; 22 | -ms-user-drag: none; 23 | user-drag: none; 24 | } 25 | 26 | textarea, input, .message { 27 | -webkit-user-select: initial; 28 | -moz-user-select: initial; 29 | -o-user-select: initial; 30 | -ms-user-select: initial; 31 | user-select: initial; 32 | } 33 | 34 | body { 35 | background: white; 36 | } 37 | 38 | header { 39 | background: rgb(245, 243, 243); 40 | border-bottom: 1px solid rgb(77, 72, 72); 41 | } 42 | 43 | .logo { 44 | width: 100px; 45 | height: 100px; 46 | vertical-align: middle; 47 | } 48 | 49 | .search-box { 50 | border: 1px solid rgb(158, 157, 157); 51 | display: inline-block; 52 | vertical-align: middle; 53 | width: 50%; 54 | overflow: hidden; 55 | background: white; 56 | } 57 | 58 | .search-box input { 59 | border:0; 60 | padding: 4px 4px; 61 | outline: none; 62 | font-size: 30px; 63 | background: transparent; 64 | color: black; 65 | width: 100%; 66 | } 67 | 68 | .icon { 69 | background-repeat:no-repeat; 70 | background-position: center center; 71 | width: 48px; 72 | height: 48px; 73 | vertical-align: middle; 74 | background-color: rgb(233, 231, 231); 75 | border: 1px solid rgb(151, 147, 147); 76 | } 77 | 78 | .icon:hover { 79 | background-color: rgb(233, 231, 201); 80 | } 81 | 82 | .icon:active { 83 | background-color: rgb(233, 231, 191); 84 | } 85 | 86 | .search-user { 87 | background-image: url('../images/search-user.png'); 88 | position: absolute; 89 | margin-top: 26px; 90 | margin-left: -47px; 91 | } 92 | 93 | .setttings { 94 | background-image: url('../images/settings.png'); 95 | } 96 | 97 | .microphone { 98 | background-image: url('../images/microphone.png'); 99 | } 100 | 101 | .camera { 102 | background-image: url('../images/camera.png'); 103 | } 104 | 105 | .share-files { 106 | background-image: url('../images/share-files.png'); 107 | } 108 | 109 | .conversation-panel { 110 | border: 1px solid rgb(158, 157, 157); 111 | margin: 10px; 112 | } 113 | 114 | .conversation { 115 | border-top: 1px solid rgb(158, 157, 157); 116 | margin-top: -1px; 117 | } 118 | 119 | .activist-name { 120 | display: inline-block; 121 | border-right: 1px solid rgb(158, 157, 157); 122 | vertical-align: middle; 123 | text-align: center; 124 | padding: 8px 4px; 125 | width: 70px; 126 | overflow: hidden; 127 | border-top: 1px solid rgb(158, 157, 157); 128 | margin-top: -1px; 129 | } 130 | 131 | .activist-name .symbol { 132 | font-size: 30px; 133 | font-weight: bolder; 134 | text-decoration: underline; 135 | } 136 | 137 | .activist-name .full-name { 138 | } 139 | 140 | .conversation .message { 141 | display: inline-block; 142 | padding: 8px 4px; 143 | vertical-align: top; 144 | max-width: 90%; 145 | border-left: 1px solid rgb(158, 157, 157); 146 | margin-left: -1px; 147 | padding-left: 10px; 148 | } 149 | .conversation .message button { 150 | font-size: 13px; 151 | padding: 0 7px; 152 | height: 16px; 153 | cursor: pointer; 154 | background: rgb(253, 253, 255); 155 | border: 1px solid rgb(226, 43, 43); 156 | border-radius: 2px; 157 | margin: 0 5px; 158 | } 159 | 160 | .conversation .message button[disabled] { 161 | cursor: default; 162 | background: transparent; 163 | border: 1px solid rgb(219, 208, 208); 164 | border-radius: 0; 165 | color: rgb(219, 208, 208); 166 | } 167 | 168 | .conversation .message input { 169 | color: black; 170 | border: 1px solid rgb(139, 131, 131); 171 | background: white; 172 | padding: 0 5px; 173 | } 174 | 175 | .conversation .message-date { 176 | font-size: 12px; 177 | color: gray; 178 | margin-top: 5px; 179 | } 180 | 181 | footer { 182 | position: fixed; 183 | bottom: 0; 184 | width: 100%; 185 | border: 1px solid rgb(158, 157, 157); 186 | background: white; 187 | border: 1px solid rgb(77, 72, 72); 188 | } 189 | 190 | .chat-input { 191 | border-right: 1px solid rgb(158, 157, 157); 192 | display: inline-block; 193 | vertical-align: middle; 194 | width: 85%; 195 | overflow: hidden; 196 | } 197 | 198 | .chat-input textarea { 199 | border:0; 200 | padding: 4px 4px; 201 | outline: none; 202 | font-size: 20px; 203 | background: transparent; 204 | resize: none; 205 | width: 99%; 206 | } 207 | 208 | footer button { 209 | background-image: url('../images/send-message.png'); 210 | } 211 | 212 | .slogan { 213 | position: absolute; 214 | margin-top: 8px; 215 | font-size: 14px; 216 | font-style: italic; 217 | height: 16px; 218 | overflow: hidden; 219 | width: 250px; 220 | } 221 | 222 | .media-panel { 223 | margin-left: 10%; 224 | display: none; 225 | margin-bottom: 1em; 226 | } 227 | 228 | .media-panel #hide-section { 229 | width: auto; 230 | padding: 0 7px; 231 | background: rgb(243, 80, 80); 232 | border: 1px solid rgb(162, 12, 12); 233 | height: 23px; 234 | color: white; 235 | cursor: pointer; 236 | border-radius: 3px 237 | } 238 | 239 | .media-panel video { 240 | width: 22%; 241 | } 242 | 243 | @media all and (max-width: 650px) { 244 | .conversation .message { 245 | max-width: 80%; 246 | } 247 | .chat-input { 248 | width: 90%; 249 | } 250 | .search-box { 251 | width: 45%; 252 | } 253 | } 254 | 255 | @media all and (max-width: 500px) { 256 | .conversation .message { 257 | max-width: 70%; 258 | } 259 | .chat-input { 260 | width: 80%; 261 | } 262 | .search-box { 263 | width: 35%; 264 | } 265 | } 266 | 267 | @media all and (max-width: 330px) { 268 | .conversation .message { 269 | max-width: 60%; 270 | } 271 | .chat-input { 272 | width: 70%; 273 | } 274 | .search-box { 275 | width: 30%; 276 | } 277 | } 278 | 279 | @media all and (max-width: 270px) { 280 | .conversation .message { 281 | max-width: 50%; 282 | } 283 | .chat-input { 284 | width: 50%; 285 | } 286 | .search-box { 287 | width: 25%; 288 | } 289 | } 290 | @media all and (max-width: 240px) { 291 | .conversation .message { 292 | max-width: 40%; 293 | } 294 | .chat-input { 295 | width: 40%; 296 | } 297 | .search-box { 298 | width: 20%; 299 | } 300 | } 301 | 302 | @media all and (max-width: 200px) { 303 | .conversation .message { 304 | max-width: 30%; 305 | } 306 | .chat-input { 307 | width: 30%; 308 | } 309 | .search-box { 310 | width: 15%; 311 | } 312 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [Conversation.js](https://github.com/muaz-khan/Conversation.js) runs top over [RTCMultiConnection.js](http://www.RTCMultiConnection.org/) [![npm](https://img.shields.io/npm/v/conversationjs.svg)](https://npmjs.org/package/conversationjs) [![downloads](https://img.shields.io/npm/dm/conversationjs.svg)](https://npmjs.org/package/conversationjs) 2 | 3 | Conversation.js is inspired by skype; and it provides simple events-like API to manage conversations, enable/disable media devices; add/download files; and do anything supported by Skype. 4 | 5 | It allows you open data conversation between two or more users using their user-ids. 6 | 7 | It is MIT Licenced, which means that you can use it in any commercial/non-commercial product, free of cost. 8 | 9 | ``` 10 | npm install conversationjs 11 | ``` 12 | 13 | To use it: 14 | 15 | ```htm 16 | 17 | ``` 18 | 19 | ## [Demos](https://www.webrtc-experiment.com/Conversationjs/) using [Conversation.js](https://github.com/muaz-khan/Conversation.js) 20 | 21 |
    22 |
  1. 23 | AndroidRTC 24 |
  2. 25 | 26 |
  3. 27 | Search Users 28 |
  4. 29 | 30 |
  5. 31 | Cross-Language (Multi-Lingual) Text Chat 32 |
  6. 33 | 34 |
  7. 35 | Old Conversation.js demos 36 |
  8. 37 |
38 | 39 | ## Gif Presentation 40 | 41 | * https://cdn.webrtc-experiment.com/images/AndroidRTC.gif 42 | 43 | ## Link the library 44 | 45 | ```html 46 | 47 | 48 | ``` 49 | 50 | = 51 | 52 | ## Open simple conversation between two users 53 | 54 | ```javascript 55 | var websocket = new WebSocket('ws://domain:port/'); 56 | 57 | // initializing constructor 58 | var signaler = new Signaler(); 59 | 60 | // whatever sent from conversation.js 61 | signaler.on('message', function(message) { 62 | // here, you received message from conversation.js 63 | // send message over WebSocket connection 64 | websocket.send(JSON.stringify(message)); 65 | }); 66 | 67 | // your websocket listener that subscribes 68 | // for all messages broadcasted from WebSockets connection 69 | websocket.onmessage = function(event) { 70 | var message = JSON.parse(event.data); 71 | 72 | // here, you received a message from websocket server 73 | // pass message to conversation.js 74 | signaler.emit('message', message); 75 | }); 76 | 77 | var user = new User(); 78 | 79 | // connect user to signaler 80 | signaler.connect(user); 81 | 82 | // invoke this method to open conversation with any user 83 | user.openconversationwith('target-username'); 84 | 85 | // this event is fired when conversation is opened 86 | user.on('conversation-opened', function (conversation) { 87 | // conversation.targetuser 88 | 89 | // emit a message to target user 90 | // conversation.emit('message', 'hello there'); 91 | 92 | conversation.on('message', function (event) { 93 | console.log(event.username, event.data); 94 | }); 95 | 96 | // enable your microphone and tell target user about it; he can 97 | // also enable his microphone or he can simply listen your voice! 98 | // conversation.emit('enable', 'microphone'); 99 | 100 | conversation.on('media-enabled', function (media) { 101 | // media.type == 'audio' || 'video' || 'screen' 102 | // media.hasmicrophone == true || null 103 | // media.hascamera == true || null 104 | // media.hasscreen == true || null 105 | // media.sender == 'string' 106 | 107 | media.emit('join-with', 'microphone'); 108 | }); 109 | }); 110 | ``` 111 | 112 | ## How to use socket.io? 113 | 114 | ```javascript 115 | var socket = io.connect(); 116 | 117 | // initializing constructor 118 | var signaler = new Signaler(); 119 | 120 | // whatever sent from conversation.js 121 | signaler.on('message', function(message) { 122 | // here, you received message from conversation.js 123 | // pass/emit message to node.js 124 | socket.emit('message', message); 125 | }); 126 | 127 | // your socket.io listener that subscribes 128 | // for all messages broadcasted from Node.js 129 | socket.on('message', function(message) { 130 | // here, you received a message from node.js server 131 | // pass message to conversation.js 132 | signaler.emit('message', message); 133 | }); 134 | 135 | // connect user to signaler 136 | signaler.connect(user); 137 | ``` 138 | 139 | ## How to use WebSockets? 140 | 141 | ```javascript 142 | var websocket = new WebSocket('ws://domain:port/'); 143 | 144 | // initializing constructor 145 | var signaler = new Signaler(); 146 | 147 | // whatever sent from conversation.js 148 | signaler.on('message', function(message) { 149 | // here, you received message from conversation.js 150 | // send message over WebSocket connection 151 | websocket.send(JSON.stringify(message)); 152 | }); 153 | 154 | // your websocket listener that subscribes 155 | // for all messages broadcasted from WebSockets connection 156 | websocket.onmessage = function(event) { 157 | var message = JSON.parse(event.data); 158 | 159 | // here, you received a message from websocket server 160 | // pass message to conversation.js 161 | signaler.emit('message', message); 162 | }); 163 | 164 | // connect user to signaler 165 | signaler.connect(user); 166 | ``` 167 | 168 | ## How to set defaults? 169 | 170 | "defaults" are default properties, objects and methods that are applied to RTCMultiConnection object. 171 | 172 | See list of all such properties here: http://www.rtcmulticonnection.org/docs/ 173 | 174 | ```javascript 175 | user.defaults = { 176 | log: true, // for production use only. 177 | trickleIce: true, // for SIP/XMPP and XHR 178 | getExternalIceServers: false, // ice-servers from xirsys.com 179 | leaveOnPageUnload: true, 180 | iceServers: [{ 181 | url: 'stun:stun.l.google.com:19302' 182 | }], 183 | iceProtocols: { 184 | tcp: true, 185 | udp: true 186 | }, 187 | candidates: { 188 | host: true, // local/host candidates 189 | reflexive: true, // STUN candidates 190 | relay: true // TURN candidates 191 | }, 192 | autoReDialOnFailure: false, // renegotiation will not work if it is true 193 | body: document.body || document.documentElement 194 | }; 195 | ``` 196 | 197 | ## How to accept/reject friend requests? 198 | 199 | ```javascript 200 | user.on('friend-request', function (request) { 201 | if (window.confirm('Do you want to accept friend-request made by ' + request.sender + '?')) { 202 | request.accept(); 203 | } else { 204 | request.reject(); 205 | } 206 | }); 207 | ``` 208 | 209 | ## How to check friend-request status? 210 | 211 | ```javascript 212 | user.on('request-status', function (request) { 213 | if (request.status == 'accepted') { 214 | alert(request.sender + ' accepted your request.'); 215 | } 216 | if (request.status == 'rejected') { 217 | alert(request.sender + ' rejected your request.'); 218 | } 219 | }); 220 | ``` 221 | 222 | ## How to emit events to multiple users? 223 | 224 | ```javascript 225 | document.querySelector('#chat-message').onchange = function (event) { 226 | user.peers.emit('message', this.value); 227 | }; 228 | 229 | document.querySelector('#enable-microphone').onclick = function () { 230 | user.peers.emit('enable', 'microphone'); 231 | }; 232 | 233 | document.querySelector('#enable-camera').onclick = function () { 234 | user.peers.emit('enable', 'camera'); 235 | }; 236 | 237 | document.querySelector('#enable-screen').onclick = function () { 238 | user.peers.emit('enable', 'screen'); 239 | }; 240 | ``` 241 | 242 | ## How to share files? 243 | 244 | ```javascript 245 | document.querySelector('input[type=file]').onchange = function () { 246 | user.peers.emit('add-file', this.files); 247 | }; 248 | ``` 249 | 250 | ## How to check if target user added file? 251 | 252 | ```javascript 253 | conversation.on('add-file', function (file) { 254 | file.download(); 255 | 256 | // or file.cancel(); 257 | }); 258 | ``` 259 | 260 | ## How to check file-download progress? 261 | 262 | ```javascript 263 | conversation.on('file-progress', function (progress) { 264 | console.log('percentage %', progress.percentage); 265 | // progress.file.name 266 | // progress.sender 267 | }); 268 | ``` 269 | 270 | ## How to save downloaded file to disk? 271 | 272 | ```javascript 273 | conversation.on('file-downloaded', function (file) { 274 | // file.sender 275 | file.savetodisk(); 276 | }); 277 | ``` 278 | 279 | ## How to check if file is successfully sent? 280 | 281 | ```javascript 282 | conversation.on('file-sent', function (file) { 283 | // file.sender 284 | console.log(file.name, 'sent.'); 285 | }); 286 | ``` 287 | 288 | ## How to check if target user refused to receive your file? 289 | 290 | ```javascript 291 | conversation.on('file-cancelled', function (file) { 292 | // file.sender 293 | console.log(file.name, 'cancelled.'); 294 | }); 295 | ``` 296 | 297 | ## Credits 298 | 299 | [Muaz Khan](https://github.com/muaz-khan): 300 | 301 | 1. Personal Webpage: http://www.muazkhan.com 302 | 2. Email: muazkh@gmail.com 303 | 3. Twitter: https://twitter.com/muazkh and https://twitter.com/WebRTCWeb 304 | 4. Google+: https://plus.google.com/+WebRTC-Experiment 305 | 5. Facebook: https://www.facebook.com/WebRTC 306 | 307 | ## License 308 | 309 | [Conversation.js](https://github.com/muaz-khan/Conversation.js) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). 310 | -------------------------------------------------------------------------------- /AndroidRTC/scripts/ui-handler.js: -------------------------------------------------------------------------------- 1 | // ui-handler for AndroidRTC app 2 | function searchInDOM(selector) { 3 | return document.querySelector(selector); 4 | } 5 | 6 | function getFromLocalStorage(key) { 7 | return localStorage.getItem(key); 8 | } 9 | 10 | function putIntoLocalStorage(key, value) { 11 | return localStorage.setItem(key, value); 12 | } 13 | 14 | window.addEventListener('DOMContentLoaded', function() { 15 | // buttons 16 | var btnSendChatMessage = searchInDOM('.send-chat-message'); 17 | var btnSearchUser = searchInDOM('.search-user'); 18 | var btnSettings = searchInDOM('.setttings'); 19 | 20 | // input boxes 21 | var chatInputBox = searchInDOM('.chat-input textarea'); 22 | var inputSearchFriends = searchInDOM('#search-friends'); 23 | 24 | var messageSound = document.getElementById('message-sound'); 25 | 26 | // containers 27 | var conversationPanel = searchInDOM('.conversation-panel'); 28 | 29 | // Conversation.js 30 | window.user = new User(); 31 | 32 | // a custom method used to append a new DIV into DOM 33 | appendConversation({ 34 | username: 'Welcome', 35 | message: 'Your username is: You can modify & share it with your friends.', 36 | callback: function(parent) { 37 | parent.querySelector('input[type=text]').onkeyup = function() { 38 | user.username = this.value; 39 | }; 40 | } 41 | }); 42 | 43 | // these are RTCMultiConnection.js defaults 44 | // RTCMultiConnection.js is big-javascript library 45 | // RTCMultiConnection provides tons of WebRTC features! 46 | // read more here: www.RTCMultiConnection.org/docs/ 47 | user.defaults = { 48 | log: false, // for production use only. 49 | trickleIce: true, // for SIP/XMPP and XHR 50 | getExternalIceServers: false, // ice-servers from xirsys.com 51 | leaveOnPageUnload: true, 52 | /*iceServers: [{ 53 | url: 'stun:stun.l.google.com:19302' 54 | }],*/ 55 | iceProtocols: { 56 | tcp: true, 57 | udp: true 58 | }, 59 | candidates: { 60 | host: true, // local/host candidates 61 | reflexive: true, // STUN candidates 62 | relay: true // TURN candidates 63 | }, 64 | autoReDialOnFailure: true, // renegotiation will not work if it is true 65 | body: document.getElementById('media-container') 66 | }; 67 | 68 | // optional method to display the logs 69 | user.on('log', function(message) { 70 | console.log(message); 71 | appendConversation({ 72 | username: 'Dev-Logs', 73 | message: message.replace(/\\r\\n/g, '
') 74 | }); 75 | }); 76 | 77 | // "signaler" object is defined in "common-signaling.js" 78 | // this method connects users to signaling-channel 79 | // so that user can call/search/invite any other user 80 | // also, other users can search him. 81 | signaler.connect(user); 82 | 83 | if (getFromLocalStorage('username')) { 84 | user.username = localStorage.getItem('username'); 85 | } else putIntoLocalStorage('username', user.username); 86 | 87 | user.on('conversation-opened', function(conversation) { 88 | conversation.emit('message', 'Hey ' + conversation.targetuser + ', conversation has been opened between you and ' + user.username + '.'); 89 | conversation.on('message', function(event) { 90 | appendConversation({ 91 | username: event.username, 92 | message: event.data 93 | }); 94 | messageSound.play(); 95 | }); 96 | conversation.on('stream', function(stream) { 97 | user.emit('--log', 'stream: ' + stream.streamid); 98 | 99 | conversation.peer.body.insertBefore(stream.mediaElement, conversation.peer.body.firstChild); 100 | 101 | btnSettings.style.display = 'none'; 102 | mediaPanel.style.display = 'block'; 103 | }); 104 | 105 | conversation.on('add-file', function(file) { 106 | appendConversation({ 107 | username: 'File', 108 | message: file.sender + ' added a file: "' + file.name + '". ', 109 | callback: function(parent) { 110 | parent.id = file.uuid; 111 | 112 | parent.querySelector('#download').onclick = function() { 113 | file.download(); 114 | disableBoth(); 115 | }; 116 | parent.querySelector('#cancel').onclick = function() { 117 | file.cancel(); 118 | disableBoth(); 119 | }; 120 | 121 | function disableBoth() { 122 | parent.querySelector('#download').disabled = true; 123 | parent.querySelector('#cancel').disabled = true; 124 | } 125 | } 126 | }); 127 | }); 128 | 129 | conversation.on('file-downloaded', function(file) { 130 | var parent = document.getElementById(file.uuid); 131 | if (!parent) return; 132 | 133 | parent.querySelector('.message').innerHTML = file.sender + ' shared a file which is downloaded in the application: "' + file.name + '". '; 134 | parent.querySelector('#save-to-disk').onclick = function() { 135 | file.savetodisk(); 136 | this.disabled = true; 137 | }; 138 | }); 139 | 140 | conversation.on('file-cancelled', function(file) { 141 | appendConversation({ 142 | username: 'Cancelled', 143 | message: conversation.targetuser + ' cancelled your file: "' + file.name + '".' 144 | }); 145 | }); 146 | 147 | conversation.on('file-sent', function(file) { 148 | appendConversation({ 149 | username: 'Sent', 150 | message: conversation.targetuser + ' received your file: "' + file.name + '".' 151 | }); 152 | }); 153 | 154 | conversation.on('file-progress', function(progress) { 155 | var parent = document.getElementById(progress.uuid); 156 | if (!parent || !parent.querySelector('#download')) return; 157 | 158 | parent.querySelector('#download').innerHTML = 'File progress percentage: ' + progress.percentage; 159 | }); 160 | }); 161 | 162 | // button used to search & invite a user 163 | btnSearchUser.onclick = function() { 164 | if (isWhiteSpace(inputSearchFriends.value)) return; 165 | user.emit('search', inputSearchFriends.value); 166 | inputSearchFriends.value = ''; 167 | }; 168 | 169 | inputSearchFriends.onkeyup = function(e) { 170 | if (e.keyCode == 13) btnSearchUser.onclick(); 171 | }; 172 | 173 | // this event is fired if a user is detected by search-agent. 174 | user.on('search', function(result) { 175 | appendConversation({ 176 | username: 'System', 177 | message: result.username + ' is online. Inviting him for chat.' 178 | }); 179 | 180 | // currently the only method to open conversation 181 | // between two users 182 | user.openconversationwith(result.username); 183 | }); 184 | 185 | btnSendChatMessage.onclick = function() { 186 | if (isWhiteSpace(chatInputBox.value)) return; 187 | user.peers.emit('message', chatInputBox.value); 188 | appendConversation({ 189 | username: user.username, 190 | message: chatInputBox.value 191 | }); 192 | chatInputBox.value = ''; 193 | }; 194 | 195 | chatInputBox.onkeyup = function(e) { 196 | if (e.keyCode != 13) return; 197 | btnSendChatMessage.onclick(); 198 | }; 199 | 200 | var mediaPanel = document.querySelector('.media-panel'); 201 | btnSettings.onclick = function() { 202 | btnSettings.style.display = 'none'; 203 | mediaPanel.style.display = 'block'; 204 | }; 205 | 206 | document.querySelector('#hide-section').onclick = function() { 207 | btnSettings.style.display = 'inline'; 208 | mediaPanel.style.display = 'none'; 209 | }; 210 | 211 | var btnEnableMicrophone = document.querySelector('.microphone'); 212 | btnEnableMicrophone.onclick = function() { 213 | btnEnableMicrophone.style.display = 'none'; 214 | user.peers.emit('enable', 'microphone'); 215 | }; 216 | 217 | var btnEnableCamera = document.querySelector('.camera'); 218 | btnEnableCamera.onclick = function() { 219 | btnEnableCamera.style.display = 'none'; 220 | user.peers.emit('enable', 'camera'); 221 | }; 222 | 223 | var btnShareFiles = document.querySelector('.share-files'); 224 | btnShareFiles.onclick = function() { 225 | var fileSelector = new FileSelector(); 226 | fileSelector.selectSingleFile(function(file) { 227 | user.peers.emit('add-file', file); 228 | }); 229 | }; 230 | 231 | function isWhiteSpace(str) { 232 | return str.replace(/^\s+|\s+$/g, '').length <= 0; 233 | } 234 | 235 | function appendConversation(args) { 236 | var conversation = document.createElement('div'); 237 | conversation.className = 'conversation'; 238 | 239 | var activistName = document.createElement('div'); 240 | activistName.className = 'activist-name'; 241 | conversation.appendChild(activistName); 242 | 243 | var symbol = document.createElement('div'); 244 | symbol.className = 'symbol'; 245 | symbol.innerHTML = args.username.split('')[0].toUpperCase(); 246 | activistName.appendChild(symbol); 247 | 248 | var fullName = document.createElement('div'); 249 | fullName.className = 'full-name'; 250 | fullName.innerHTML = args.username; 251 | activistName.appendChild(fullName); 252 | 253 | var message = document.createElement('div'); 254 | message.className = 'message'; 255 | message.innerHTML = args.message; 256 | conversation.appendChild(message); 257 | 258 | var messageDate = document.createElement('div'); 259 | messageDate.className = 'message-date'; 260 | messageDate.innerHTML = getCurrentTime(); 261 | message.appendChild(messageDate); 262 | 263 | conversationPanel.insertBefore(conversation, conversationPanel.firstChild); 264 | 265 | if (args.callback) args.callback(conversation); 266 | } 267 | 268 | function getCurrentTime() { 269 | var date = new Date(); 270 | var hours = date.getHours(); 271 | var minutes = date.getMinutes(); 272 | var seconds = date.getSeconds(); 273 | 274 | if (hours.toString().length == 1) hours = '0' + hours; 275 | if (minutes.toString().length == 1) minutes = '0' + minutes; 276 | if (seconds.toString().length == 1) seconds = '0' + seconds; 277 | 278 | return hours + ':' + minutes + ':' + seconds; 279 | } 280 | }, false); 281 | -------------------------------------------------------------------------------- /AndroidRTC/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AndroidRTC: Runs top over Conversation.js! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | AndroidRTC: Runs top over Conversation.js! 34 | 37 | 38 | 39 | 40 | 41 |
42 |
43 | Click to enable: 44 | 45 | 46 | 47 | 48 |
49 |
50 |
51 |
52 | 53 |
54 | 55 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /demos/search-user.html: -------------------------------------------------------------------------------- 1 | Search Users using Conversation.js ® Muaz Khan 2 | 3 |

4 | Search Users using Conversation.js 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 109 | 110 | 111 | 211 | 212 | 219 | -------------------------------------------------------------------------------- /demos/cross-language-chat.html: -------------------------------------------------------------------------------- 1 | Text-Translation (Multi-Lingual) using Conversation.js ® Muaz Khan 2 | 3 |

4 | Text-Translation (Multi-Lingual) using Conversation.js 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 118 | 119 | 120 | 212 | 213 | 220 | -------------------------------------------------------------------------------- /conversation.js: -------------------------------------------------------------------------------- 1 | // Last time updated at Sep 15, 2014, 08:32:23 2 | 3 | // Latest file can be found here: https://cdn.webrtc-experiment.com/conversation.js 4 | 5 | // Muaz Khan - www.MuazKhan.com 6 | // MIT License - www.WebRTC-Experiment.com/licence 7 | // _______________ 8 | // Conversation.js 9 | 10 | /* 11 | -. Signaler object added. See below for how to use it. 12 | 13 | var signaler = new Signaler(); 14 | signaler.on('message', function(message) { 15 | socket.send(message); 16 | }); 17 | 18 | socket.on('message', function(message) { 19 | signaler.emit('message', message); 20 | }); 21 | 22 | var user = new User(); 23 | 24 | signaler.connect(user); 25 | */ 26 | 27 | (function() { 28 | window.Signaler = function() { 29 | var signaler = this; 30 | var users = []; 31 | signaler.connect = function(user) { 32 | user.signaler = signaler; 33 | user.emit('--signaler-connected'); 34 | users.push(user); 35 | }; 36 | 37 | signaler.send = function(data) { 38 | signaler.emit('--message', data); 39 | }; 40 | 41 | signaler.events = {}; 42 | signaler.on = function(event, callback) { 43 | signaler.events[event] = callback; 44 | }; 45 | 46 | signaler.emit = function() { 47 | if (!arguments[0]) throw 'At least one argument is mandatory.'; 48 | 49 | var internalevent = arguments[0].indexOf('--') == 0; 50 | if (!internalevent) { 51 | if (arguments[0] == 'message') { 52 | var message = arguments[1]; 53 | users.forEach(function(user) { 54 | user.onsignalingmessage(message); 55 | }); 56 | } 57 | 58 | return; 59 | } 60 | 61 | if (internalevent) { 62 | arguments[0] = arguments[0].replace('--', ''); 63 | } 64 | 65 | if (!signaler.events[arguments[0]]) { 66 | var warning = 'Event name "' + arguments[0] + '" doesn\'t exists.'; 67 | if (arguments[1]) warning += ' Values: ' + JSON.stringify(arguments[1], null, '\t'); 68 | console.warn(warning); 69 | 70 | } else signaler.events[arguments[0]](arguments[1], arguments[2], arguments[3], arguments[4]); 71 | }; 72 | }; 73 | 74 | window.User = function(_defaults) { 75 | var user = this; 76 | 77 | user.randomstring = function() { 78 | // suggested by @rvulpescu from #154 79 | if (window.crypto && crypto.getRandomValues && navigator.userAgent.indexOf('Safari') == -1) { 80 | var a = window.crypto.getRandomValues(new Uint32Array(3)), 81 | token = ''; 82 | for (var i = 0, l = a.length; i < l; i++) { 83 | token += a[i].toString(36); 84 | } 85 | return token; 86 | } else { 87 | return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); 88 | } 89 | }; 90 | 91 | user.defaults = {}; 92 | user.username = user.randomstring(); 93 | user.status = 'online'; 94 | 95 | user.openconversationwith = function(targetuser) { 96 | var conversation = new Conversation(user, targetuser); 97 | 98 | // check who is online 99 | user.emit('signal', { 100 | whoisonline: true, 101 | responsefor: targetuser 102 | }); 103 | }; 104 | 105 | user.onmessagecallbacks = {}; 106 | user.onsignalingmessage = function(message) { 107 | if (message.isrtcmulticonnectioninnermessage && user.onmessagecallbacks[message.channel]) { 108 | return user.onmessagecallbacks[message.channel](message.message); 109 | } 110 | 111 | if (message.searchuser) { 112 | if (user.status === 'online') { 113 | user.emit('signal', { 114 | useronline: true, 115 | responsefor: message.sender 116 | }); 117 | } 118 | return; 119 | } 120 | 121 | if (message.useronline && message.responsefor == user.username) { 122 | user.emit('--search', { 123 | username: message.sender 124 | }); 125 | return; 126 | } 127 | 128 | if (message.whoisonline && message.responsefor == user.username && user.status == 'online') { 129 | user.emit('--friend-request', { 130 | accept: function() { 131 | if (!user.peers[message.sender]) { 132 | var randomchannel = user.randomstring(); 133 | user.emit('signal', { 134 | iamonline: true, 135 | responsefor: message.sender, 136 | randomchannel: randomchannel 137 | }); 138 | 139 | var conversation = new Conversation(user, message.sender); 140 | 141 | conversation.addnewrtcmulticonnectionpeer({ 142 | targetuser: message.sender, 143 | channel: randomchannel 144 | }); 145 | } 146 | 147 | user.emit('signal', { 148 | requestaccepted: true, 149 | sender: user.username, 150 | responsefor: message.sender 151 | }); 152 | }, 153 | reject: function() { 154 | user.emit('signal', { 155 | requestrejected: true, 156 | sender: user.username, 157 | responsefor: message.sender 158 | }); 159 | }, 160 | sender: message.sender 161 | }); 162 | } 163 | 164 | if (message.requestaccepted && message.responsefor == user.username) { 165 | user.emit('--request-status', { 166 | status: 'accepted', 167 | sender: message.sender 168 | }); 169 | } 170 | 171 | if (message.requestrejected && message.responsefor == user.username) { 172 | user.emit('--request-status', { 173 | status: 'accepted', 174 | sender: message.sender 175 | }); 176 | } 177 | 178 | if (message.iamonline && message.responsefor == user.username) { 179 | if (!user.peers[message.sender]) { 180 | user.conversations[message.sender].addnewrtcmulticonnectionpeer({ 181 | targetuser: message.sender, 182 | channel: message.randomchannel, 183 | isInitiator: true 184 | }); 185 | } 186 | } 187 | 188 | if (message.sessionDescription && message.responsefor == user.username) { 189 | user.peers[message.sender].join(message.sessionDescription); 190 | } 191 | } 192 | 193 | // reference to all RTCMultiConnection peers 194 | user.peers = { 195 | emit: function(eventName, value) { 196 | for (var conversation in user.conversations) { 197 | user.conversations[conversation].emit(eventName, value); 198 | } 199 | }, 200 | length: 0 201 | }; 202 | 203 | // reference to all Conversation objects 204 | user.conversations = {}; 205 | 206 | // reference to all local media streams 207 | user.localstreams = {}; 208 | 209 | user.events = {}; 210 | user.on = function(event, callback) { 211 | user.events[event] = callback; 212 | }; 213 | 214 | user.emit = function() { 215 | if (!arguments[0]) throw 'At least one argument is mandatory.'; 216 | 217 | var internalevent = arguments[0].indexOf('--') == 0; 218 | if (!internalevent) { 219 | if (arguments[0] == 'search') { 220 | user.emit('signal', { 221 | searchuser: arguments[1] 222 | }); 223 | } 224 | 225 | if (arguments[0] == 'signal') { 226 | var data = arguments[1]; 227 | data.sender = user.username; 228 | user.signaler.emit('--message', data); 229 | } 230 | 231 | return; 232 | } 233 | 234 | if (internalevent) { 235 | arguments[0] = arguments[0].replace('--', ''); 236 | if (arguments[0] == 'log' && user.defaults.log == false) return; 237 | } 238 | 239 | if (!user.events[arguments[0]]) { 240 | var warning = 'Event name "' + arguments[0] + '" doesn\'t exists.'; 241 | if (arguments[1]) warning += ' Values: ' + JSON.stringify(arguments[1], null, '\t'); 242 | console.warn(warning); 243 | 244 | } else user.events[arguments[0]](arguments[1], arguments[2], arguments[3], arguments[4]); 245 | }; 246 | 247 | // defaults 248 | user.on('request-status', function(request) { 249 | user.emit('--log', request.sender + ' ' + request.status + ' your request.'); 250 | }); 251 | 252 | user.on('friend-request', function(request) { 253 | request.accept(); 254 | }); 255 | 256 | user.on('signaler-connected', function() { 257 | user.emit('--log', 'Signaling medium is ready for pub/sub.'); 258 | }); 259 | 260 | user.on('log', function(message) { 261 | console.log(message); 262 | }); 263 | 264 | for (var data in _defaults || {}) { 265 | user.defaults[data] = _defaults[data]; 266 | } 267 | 268 | if (_defaults) { 269 | user.emit('signaler', 'start'); 270 | } 271 | }; 272 | 273 | function Conversation(user, targetuser) { 274 | var conversation = this; 275 | 276 | function sendmessage(data) { 277 | conversation.peer.send(data); 278 | } 279 | 280 | function enablemicrophone() { 281 | if (user.localstreams.microphone) return; 282 | 283 | conversation.peer.captureUserMedia(function(stream) { 284 | user.localstreams.microphone = stream; 285 | 286 | conversation.peer.peers[targetuser].peer.connection.addStream(stream); 287 | 288 | conversation.peer.send({ 289 | signalingmessage: true, 290 | hasmicrophone: true, 291 | streamavailable: true, 292 | sender: user.username, 293 | type: 'audio' 294 | }); 295 | }, { 296 | audio: true 297 | }); 298 | } 299 | 300 | function enablecamera() { 301 | if (user.localstreams.camera) return; 302 | 303 | conversation.peer.captureUserMedia(function(stream) { 304 | user.localstreams.camera = stream; 305 | conversation.peer.peers[targetuser].peer.connection.addStream(stream); 306 | 307 | conversation.peer.send({ 308 | signalingmessage: true, 309 | hascamera: true, 310 | streamavailable: true, 311 | sender: user.username, 312 | type: 'video' 313 | }); 314 | }, { 315 | audio: true, 316 | video: true 317 | }); 318 | } 319 | 320 | function enablescreen() { 321 | if (user.localstreams.screen) return; 322 | 323 | conversation.peer.captureUserMedia(function(stream) { 324 | user.localstreams.screen = stream; 325 | 326 | conversation.peer.peers[targetuser].peer.connection.addStream(stream); 327 | 328 | conversation.peer.send({ 329 | signalingmessage: true, 330 | hasscreen: true, 331 | streamavailable: true, 332 | sender: user.username, 333 | type: 'screen' 334 | }); 335 | }, { 336 | screen: true 337 | }); 338 | }; 339 | 340 | conversation.attachie = { 341 | files: {}, 342 | messages: {} 343 | }; 344 | 345 | conversation.addnewrtcmulticonnectionpeer = function(args) { 346 | var connection = new RTCMultiConnection(args.channel); 347 | 348 | for (var d in user.defaults) { 349 | if (typeof connection[d] !== 'undefined' /* && typeof connection[d] !== 'function' */ ) { 350 | connection[d] = user.defaults[d]; 351 | } 352 | } 353 | 354 | // workaround for RTCMultiConnection-v1.8 or older 355 | if (user.defaults.log == false) { 356 | connection.skipLogs(); 357 | } 358 | 359 | connection.userid = user.username; 360 | 361 | // v1.9 and onwards supports "onlog" event. 362 | connection.onlog = function(message) { 363 | user.emit('--log', JSON.stringify(message, null, '\t')); 364 | }; 365 | 366 | connection.session = { 367 | data: true 368 | }; 369 | 370 | connection.onopen = function() { 371 | conversation.targetuser = args.targetuser; 372 | user.emit('--conversation-opened', conversation); 373 | }; 374 | 375 | connection.onmessage = function(event) { 376 | if (event.data.signalingmessage) { 377 | var data = event.data; 378 | 379 | if (data.streamavailable) { 380 | data.emit = function(first, second) { 381 | if (first == 'join-with' && second == 'nothing') { 382 | preview(); 383 | } 384 | 385 | if (first == 'join-with' && second == 'microphone') { 386 | joinwithmicrophone(); 387 | } 388 | 389 | if (first == 'join-with' && second == 'camera') { 390 | joinwithcamera(); 391 | } 392 | 393 | if (first == 'join-with' && second == 'screen') { 394 | joinwithscreen(); 395 | } 396 | }; 397 | 398 | function preview() { 399 | connection.send({ 400 | signalingmessage: true, 401 | shareoneway: true, 402 | hasmicrophone: !!data.hasmicrophone, 403 | hascamera: !!data.hascamera, 404 | hasscreen: !!data.hasscreen 405 | }); 406 | } 407 | 408 | function joinwithmicrophone() { 409 | conversation.peer.peers[targetuser].addStream({ 410 | audio: true, 411 | oneway: true 412 | }); 413 | } 414 | 415 | function joinwithcamera() { 416 | conversation.peer.peers[targetuser].addStream({ 417 | audio: true, 418 | video: true, 419 | oneway: true 420 | }); 421 | } 422 | 423 | function joinwithscreen() { 424 | conversation.peer.peers[targetuser].addStream({ 425 | screen: true, 426 | oneway: true 427 | }); 428 | } 429 | 430 | conversation.emit('--media-enabled', event.data); 431 | } 432 | 433 | if (data.shareoneway) { 434 | if (data.hasmicrophone) { 435 | if (!user.localstreams.microphone) throw 'You have not allowed microphone.'; 436 | connection.renegotiate(); 437 | } 438 | 439 | if (data.hascamera) { 440 | if (!user.localstreams.camera) throw 'You have not allowed camera.'; 441 | connection.renegotiate(); 442 | } 443 | 444 | if (data.hasscreen) { 445 | if (!user.localstreams.screen) throw 'You have not allowed screen.'; 446 | connection.renegotiate(); 447 | } 448 | } 449 | 450 | if (data.addedfile) { 451 | var filesinfo = data.filesinfo; 452 | if (filesinfo.size && filesinfo.type) { 453 | filesinfo = eventHanlders(filesinfo); 454 | conversation.emit('--add-file', filesinfo); 455 | } else { 456 | for (var file in filesinfo) { 457 | filesinfo[file] = eventHanlders(filesinfo[file]); 458 | conversation.emit('--add-file', filesinfo[file]); 459 | } 460 | } 461 | 462 | function eventHanlders(file) { 463 | file.sender = targetuser; 464 | 465 | file.download = function() { 466 | conversation.peer.send({ 467 | signalingmessage: true, 468 | download: true, 469 | sender: user.username, 470 | file: { 471 | size: file.size, 472 | type: file.type, 473 | name: file.name, 474 | lastModifiedDate: file.lastModifiedDate, 475 | uuid: file.uuid 476 | } 477 | }); 478 | }; 479 | 480 | file.cancel = function() { 481 | conversation.peer.send({ 482 | signalingmessage: true, 483 | download: false, 484 | sender: user.username, 485 | file: { 486 | size: file.size, 487 | type: file.type, 488 | name: file.name, 489 | lastModifiedDate: file.lastModifiedDate, 490 | uuid: file.uuid 491 | } 492 | }); 493 | }; 494 | 495 | return file; 496 | } 497 | } 498 | 499 | if (data.file) { 500 | if (data.download) { 501 | var file = conversation.attachie.files[data.file.name]; 502 | if ((file.item && file.length && file[0] && file[0].lastModifiedDate) || file.forEach) { 503 | Array.prototype.slice.call(file).forEach(function(f) { 504 | conversation.peer.send(f); 505 | }); 506 | } else conversation.peer.send(file); 507 | } else { 508 | var file = conversation.attachie.files[data.file.name]; 509 | conversation.emit('--file-cancelled', file); 510 | } 511 | } 512 | } else { 513 | event.username = conversation.targetuser; 514 | conversation.emit('--message', event); 515 | } 516 | }; 517 | 518 | // overriding "openSignalingChannel" method 519 | connection.openSignalingChannel = function(config) { 520 | var channel = config.channel || this.channel; 521 | 522 | user.onmessagecallbacks[channel] = config.onmessage; 523 | 524 | if (config.onopen) setTimeout(config.onopen, 1000); 525 | 526 | // directly returning socket object using "return" statement 527 | return { 528 | send: function(message) { 529 | user.emit('signal', { 530 | message: message, 531 | isrtcmulticonnectioninnermessage: true, 532 | channel: channel 533 | }); 534 | }, 535 | channel: channel 536 | }; 537 | }; 538 | 539 | // todo: renegotiation doesn't work if trickleIce=false 540 | // need to fix it. 541 | // connection.trickleIce = false; 542 | 543 | connection.sdpConstraints.mandatory = { 544 | OfferToReceiveAudio: true, 545 | OfferToReceiveVideo: true 546 | }; 547 | 548 | connection.autoSaveToDisk = false; 549 | 550 | connection.onFileEnd = function(file) { 551 | if (conversation.attachie.files[file.name]) { 552 | conversation.emit('--file-sent', file); 553 | return; 554 | } 555 | 556 | file.savetodisk = function(filename) { 557 | connection.saveToDisk(file, filename || file.name || file.type); 558 | }; 559 | 560 | file.sender = file.userid; 561 | 562 | conversation.emit('--file-downloaded', file); 563 | }; 564 | 565 | connection.onFileProgress = function(chunk) { 566 | var progress = { 567 | percentage: Math.round((chunk.currentPosition / chunk.maxChunks) * 100), 568 | uuid: chunk.uuid, 569 | file: chunk, 570 | sender: chunk.userid 571 | }; 572 | 573 | if (progress.percentage > 100) progress.percentage = 100; 574 | 575 | conversation.emit('--file-progress', progress); 576 | }; 577 | 578 | connection.onFileStart = function() {}; 579 | 580 | connection.onstreamid = function(event) { 581 | conversation.emit('--stream-clue', event); 582 | }; 583 | 584 | connection.onstream = function(event) { 585 | conversation.emit('--stream', event); 586 | }; 587 | 588 | connection.onstreamended = function(event) { 589 | conversation.emit('--stream-ended', event); 590 | }; 591 | 592 | if (args.isInitiator) { 593 | connection.open({ 594 | dontTransmit: true 595 | }); 596 | } 597 | 598 | user.peers[args.targetuser] = connection; 599 | 600 | user.peers.length++; 601 | connection.onleave = function() { 602 | user.peers.length--; 603 | }; 604 | 605 | user.emit('signal', { 606 | joinRoom: true, 607 | responsefor: args.targetuser, 608 | sessionDescription: connection.sessionDescription 609 | }); 610 | 611 | conversation.peer = connection; 612 | } 613 | 614 | function addfile(file) { 615 | var filesinfo = {}; 616 | 617 | // seems array of files 618 | if ((file.item && file.length && file[0] && file[0].lastModifiedDate) || file.forEach) { 619 | Array.prototype.slice.call(file).forEach(function(f) { 620 | if (!f.uuid) f.uuid = user.randomstring(); 621 | filesinfo[file.name] = { 622 | size: f.size, 623 | type: f.type, 624 | name: f.name, 625 | lastModifiedDate: f.lastModifiedDate, 626 | uuid: f.uuid 627 | }; 628 | conversation.attachie.files[f.name] = f; 629 | }); 630 | } else { 631 | if (!file.uuid) file.uuid = user.randomstring(); 632 | filesinfo[file.name] = { 633 | size: file.size, 634 | type: file.type, 635 | name: file.name, 636 | lastModifiedDate: file.lastModifiedDate, 637 | uuid: file.uuid 638 | }; 639 | 640 | conversation.attachie.files[file.name] = file; 641 | } 642 | 643 | conversation.peer.send({ 644 | addedfile: true, 645 | filesinfo: filesinfo, 646 | signalingmessage: true, 647 | sender: user.username 648 | }); 649 | } 650 | 651 | // custom events 652 | 653 | conversation.events = {}; 654 | conversation.on = function(event, callback) { 655 | conversation.events[event] = callback; 656 | }; 657 | 658 | conversation.emit = function() { 659 | if (!arguments[0]) throw 'At least one argument is mandatory.'; 660 | 661 | var internalevent = arguments[0].indexOf('--') == 0; 662 | if (!internalevent) { 663 | if (arguments[0] == 'message') { 664 | sendmessage(arguments[1]); 665 | } 666 | 667 | if (arguments[0] == 'add-file' && arguments[1]) { 668 | addfile(arguments[1]); 669 | } 670 | 671 | if (arguments[0] == 'enable') { 672 | if (arguments[1] == 'microphone') { 673 | enablemicrophone(); 674 | } 675 | 676 | if (arguments[1] == 'camera') { 677 | enablecamera(); 678 | } 679 | 680 | if (arguments[1] == 'screen') { 681 | enablescreen(); 682 | } 683 | } 684 | 685 | if (arguments[0] == 'end') { 686 | conversation.peer.close(); 687 | user.emit('--ended'); 688 | } 689 | 690 | return; 691 | } 692 | 693 | if (internalevent) { 694 | arguments[0] = arguments[0].replace('--', ''); 695 | } 696 | 697 | if (!conversation.events[arguments[0]]) { 698 | var warning = 'Event name "' + arguments[0] + '" doesn\'t exists.'; 699 | if (arguments[1]) warning += ' Values: ' + JSON.stringify(arguments[1], null, '\t'); 700 | console.warn(warning); 701 | 702 | } else conversation.events[arguments[0]](arguments[1], arguments[2], arguments[3], arguments[4]); 703 | }; 704 | 705 | conversation.on('add-file', function(file) { 706 | file.download(); 707 | }); 708 | 709 | conversation.on('file-downloaded', function(file) { 710 | file.savetodisk(); 711 | }); 712 | 713 | conversation.on('media-enabled', function(media) { 714 | if (media.hasmicrophone) { 715 | media.emit('join-with', 'microphone'); 716 | } 717 | 718 | if (media.hascamera) { 719 | media.emit('join-with', 'camera'); 720 | } 721 | 722 | if (media.hasscreen) { 723 | media.emit('join-with', 'nothing'); 724 | } 725 | }); 726 | 727 | conversation.on('stream-clue', function(stream) { 728 | user.emit('--log', 'stream-clue: ' + stream.streamid); 729 | }); 730 | 731 | conversation.on('stream', function(stream) { 732 | user.emit('--log', 'stream: ' + stream.streamid); 733 | 734 | conversation.peer.body.insertBefore(stream.mediaElement, conversation.peer.body.firstChild); 735 | }); 736 | 737 | conversation.on('stream-ended', function(stream) { 738 | user.emit('--log', 'stream-ended: ' + stream.streamid); 739 | 740 | if (stream.mediaElement && stream.mediaElement.parentNode) { 741 | stream.mediaElement.parentNode.removeChild(stream.mediaElement); 742 | } 743 | }); 744 | 745 | user.conversations[targetuser] = conversation; 746 | } 747 | })(); 748 | -------------------------------------------------------------------------------- /AndroidRTC/scripts/conversation.js: -------------------------------------------------------------------------------- 1 | // Last time updated at Sep 15, 2014, 08:32:23 2 | 3 | // Latest file can be found here: https://cdn.webrtc-experiment.com/conversation.js 4 | 5 | // Muaz Khan - www.MuazKhan.com 6 | // MIT License - www.WebRTC-Experiment.com/licence 7 | // _______________ 8 | // Conversation.js 9 | 10 | /* 11 | -. Signaler object added. See below for how to use it. 12 | 13 | var signaler = new Signaler(); 14 | signaler.on('message', function(message) { 15 | socket.send(message); 16 | }); 17 | 18 | socket.on('message', function(message) { 19 | signaler.emit('message', message); 20 | }); 21 | 22 | var user = new User(); 23 | 24 | signaler.connect(user); 25 | */ 26 | 27 | (function() { 28 | window.Signaler = function() { 29 | var signaler = this; 30 | var users = []; 31 | signaler.connect = function(user) { 32 | user.signaler = signaler; 33 | user.emit('--signaler-connected'); 34 | users.push(user); 35 | }; 36 | 37 | signaler.send = function(data) { 38 | signaler.emit('--message', data); 39 | }; 40 | 41 | signaler.events = {}; 42 | signaler.on = function(event, callback) { 43 | signaler.events[event] = callback; 44 | }; 45 | 46 | signaler.emit = function() { 47 | if (!arguments[0]) throw 'At least one argument is mandatory.'; 48 | 49 | var internalevent = arguments[0].indexOf('--') == 0; 50 | if (!internalevent) { 51 | if (arguments[0] == 'message') { 52 | var message = arguments[1]; 53 | users.forEach(function(user) { 54 | user.onsignalingmessage(message); 55 | }); 56 | } 57 | 58 | return; 59 | } 60 | 61 | if (internalevent) { 62 | arguments[0] = arguments[0].replace('--', ''); 63 | } 64 | 65 | if (!signaler.events[arguments[0]]) { 66 | var warning = 'Event name "' + arguments[0] + '" doesn\'t exists.'; 67 | if (arguments[1]) warning += ' Values: ' + JSON.stringify(arguments[1], null, '\t'); 68 | console.warn(warning); 69 | 70 | } else signaler.events[arguments[0]](arguments[1], arguments[2], arguments[3], arguments[4]); 71 | }; 72 | }; 73 | 74 | window.User = function(_defaults) { 75 | var user = this; 76 | 77 | user.randomstring = function() { 78 | // suggested by @rvulpescu from #154 79 | if (window.crypto && crypto.getRandomValues && navigator.userAgent.indexOf('Safari') == -1) { 80 | var a = window.crypto.getRandomValues(new Uint32Array(3)), 81 | token = ''; 82 | for (var i = 0, l = a.length; i < l; i++) { 83 | token += a[i].toString(36); 84 | } 85 | return token; 86 | } else { 87 | return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); 88 | } 89 | }; 90 | 91 | user.defaults = {}; 92 | user.username = user.randomstring(); 93 | user.status = 'online'; 94 | 95 | user.openconversationwith = function(targetuser) { 96 | var conversation = new Conversation(user, targetuser); 97 | 98 | // check who is online 99 | user.emit('signal', { 100 | whoisonline: true, 101 | responsefor: targetuser 102 | }); 103 | }; 104 | 105 | user.onmessagecallbacks = {}; 106 | user.onsignalingmessage = function(message) { 107 | if (message.isrtcmulticonnectioninnermessage && user.onmessagecallbacks[message.channel]) { 108 | return user.onmessagecallbacks[message.channel](message.message); 109 | } 110 | 111 | if (message.searchuser) { 112 | if (user.status === 'online') { 113 | user.emit('signal', { 114 | useronline: true, 115 | responsefor: message.sender 116 | }); 117 | } 118 | return; 119 | } 120 | 121 | if (message.useronline && message.responsefor == user.username) { 122 | user.emit('--search', { 123 | username: message.sender 124 | }); 125 | return; 126 | } 127 | 128 | if (message.whoisonline && message.responsefor == user.username && user.status == 'online') { 129 | user.emit('--friend-request', { 130 | accept: function() { 131 | if (!user.peers[message.sender]) { 132 | var randomchannel = user.randomstring(); 133 | user.emit('signal', { 134 | iamonline: true, 135 | responsefor: message.sender, 136 | randomchannel: randomchannel 137 | }); 138 | 139 | var conversation = new Conversation(user, message.sender); 140 | 141 | conversation.addnewrtcmulticonnectionpeer({ 142 | targetuser: message.sender, 143 | channel: randomchannel 144 | }); 145 | } 146 | 147 | user.emit('signal', { 148 | requestaccepted: true, 149 | sender: user.username, 150 | responsefor: message.sender 151 | }); 152 | }, 153 | reject: function() { 154 | user.emit('signal', { 155 | requestrejected: true, 156 | sender: user.username, 157 | responsefor: message.sender 158 | }); 159 | }, 160 | sender: message.sender 161 | }); 162 | } 163 | 164 | if (message.requestaccepted && message.responsefor == user.username) { 165 | user.emit('--request-status', { 166 | status: 'accepted', 167 | sender: message.sender 168 | }); 169 | } 170 | 171 | if (message.requestrejected && message.responsefor == user.username) { 172 | user.emit('--request-status', { 173 | status: 'accepted', 174 | sender: message.sender 175 | }); 176 | } 177 | 178 | if (message.iamonline && message.responsefor == user.username) { 179 | if (!user.peers[message.sender]) { 180 | user.conversations[message.sender].addnewrtcmulticonnectionpeer({ 181 | targetuser: message.sender, 182 | channel: message.randomchannel, 183 | isInitiator: true 184 | }); 185 | } 186 | } 187 | 188 | if (message.sessionDescription && message.responsefor == user.username) { 189 | user.peers[message.sender].join(message.sessionDescription); 190 | } 191 | } 192 | 193 | // reference to all RTCMultiConnection peers 194 | user.peers = { 195 | emit: function(eventName, value) { 196 | for (var conversation in user.conversations) { 197 | user.conversations[conversation].emit(eventName, value); 198 | } 199 | }, 200 | length: 0 201 | }; 202 | 203 | // reference to all Conversation objects 204 | user.conversations = {}; 205 | 206 | // reference to all local media streams 207 | user.localstreams = {}; 208 | 209 | user.events = {}; 210 | user.on = function(event, callback) { 211 | user.events[event] = callback; 212 | }; 213 | 214 | user.emit = function() { 215 | if (!arguments[0]) throw 'At least one argument is mandatory.'; 216 | 217 | var internalevent = arguments[0].indexOf('--') == 0; 218 | if (!internalevent) { 219 | if (arguments[0] == 'search') { 220 | user.emit('signal', { 221 | searchuser: arguments[1] 222 | }); 223 | } 224 | 225 | if (arguments[0] == 'signal') { 226 | var data = arguments[1]; 227 | data.sender = user.username; 228 | user.signaler.emit('--message', data); 229 | } 230 | 231 | return; 232 | } 233 | 234 | if (internalevent) { 235 | arguments[0] = arguments[0].replace('--', ''); 236 | if (arguments[0] == 'log' && user.defaults.log == false) return; 237 | } 238 | 239 | if (!user.events[arguments[0]]) { 240 | var warning = 'Event name "' + arguments[0] + '" doesn\'t exists.'; 241 | if (arguments[1]) warning += ' Values: ' + JSON.stringify(arguments[1], null, '\t'); 242 | console.warn(warning); 243 | 244 | } else user.events[arguments[0]](arguments[1], arguments[2], arguments[3], arguments[4]); 245 | }; 246 | 247 | // defaults 248 | user.on('request-status', function(request) { 249 | user.emit('--log', request.sender + ' ' + request.status + ' your request.'); 250 | }); 251 | 252 | user.on('friend-request', function(request) { 253 | request.accept(); 254 | }); 255 | 256 | user.on('signaler-connected', function() { 257 | user.emit('--log', 'Signaling medium is ready for pub/sub.'); 258 | }); 259 | 260 | user.on('log', function(message) { 261 | console.log(message); 262 | }); 263 | 264 | for (var data in _defaults || {}) { 265 | user.defaults[data] = _defaults[data]; 266 | } 267 | 268 | if (_defaults) { 269 | user.emit('signaler', 'start'); 270 | } 271 | }; 272 | 273 | function Conversation(user, targetuser) { 274 | var conversation = this; 275 | 276 | function sendmessage(data) { 277 | conversation.peer.send(data); 278 | } 279 | 280 | function enablemicrophone() { 281 | if (user.localstreams.microphone) return; 282 | 283 | conversation.peer.captureUserMedia(function(stream) { 284 | user.localstreams.microphone = stream; 285 | 286 | conversation.peer.peers[targetuser].peer.connection.addStream(stream); 287 | 288 | conversation.peer.send({ 289 | signalingmessage: true, 290 | hasmicrophone: true, 291 | streamavailable: true, 292 | sender: user.username, 293 | type: 'audio' 294 | }); 295 | }, { 296 | audio: true 297 | }); 298 | } 299 | 300 | function enablecamera() { 301 | if (user.localstreams.camera) return; 302 | 303 | conversation.peer.captureUserMedia(function(stream) { 304 | user.localstreams.camera = stream; 305 | conversation.peer.peers[targetuser].peer.connection.addStream(stream); 306 | 307 | conversation.peer.send({ 308 | signalingmessage: true, 309 | hascamera: true, 310 | streamavailable: true, 311 | sender: user.username, 312 | type: 'video' 313 | }); 314 | }, { 315 | audio: true, 316 | video: true 317 | }); 318 | } 319 | 320 | function enablescreen() { 321 | if (user.localstreams.screen) return; 322 | 323 | conversation.peer.captureUserMedia(function(stream) { 324 | user.localstreams.screen = stream; 325 | 326 | conversation.peer.peers[targetuser].peer.connection.addStream(stream); 327 | 328 | conversation.peer.send({ 329 | signalingmessage: true, 330 | hasscreen: true, 331 | streamavailable: true, 332 | sender: user.username, 333 | type: 'screen' 334 | }); 335 | }, { 336 | screen: true 337 | }); 338 | }; 339 | 340 | conversation.attachie = { 341 | files: {}, 342 | messages: {} 343 | }; 344 | 345 | conversation.addnewrtcmulticonnectionpeer = function(args) { 346 | var connection = new RTCMultiConnection(args.channel); 347 | 348 | for (var d in user.defaults) { 349 | if (typeof connection[d] !== 'undefined' /* && typeof connection[d] !== 'function' */ ) { 350 | connection[d] = user.defaults[d]; 351 | } 352 | } 353 | 354 | // workaround for RTCMultiConnection-v1.8 or older 355 | if (user.defaults.log == false) { 356 | connection.skipLogs(); 357 | } 358 | 359 | connection.userid = user.username; 360 | 361 | // v1.9 and onwards supports "onlog" event. 362 | connection.onlog = function(message) { 363 | user.emit('--log', JSON.stringify(message, null, '\t')); 364 | }; 365 | 366 | connection.session = { 367 | data: true 368 | }; 369 | 370 | connection.onopen = function() { 371 | conversation.targetuser = args.targetuser; 372 | user.emit('--conversation-opened', conversation); 373 | }; 374 | 375 | connection.onmessage = function(event) { 376 | if (event.data.signalingmessage) { 377 | var data = event.data; 378 | 379 | if (data.streamavailable) { 380 | data.emit = function(first, second) { 381 | if (first == 'join-with' && second == 'nothing') { 382 | preview(); 383 | } 384 | 385 | if (first == 'join-with' && second == 'microphone') { 386 | joinwithmicrophone(); 387 | } 388 | 389 | if (first == 'join-with' && second == 'camera') { 390 | joinwithcamera(); 391 | } 392 | 393 | if (first == 'join-with' && second == 'screen') { 394 | joinwithscreen(); 395 | } 396 | }; 397 | 398 | function preview() { 399 | connection.send({ 400 | signalingmessage: true, 401 | shareoneway: true, 402 | hasmicrophone: !!data.hasmicrophone, 403 | hascamera: !!data.hascamera, 404 | hasscreen: !!data.hasscreen 405 | }); 406 | } 407 | 408 | function joinwithmicrophone() { 409 | conversation.peer.peers[targetuser].addStream({ 410 | audio: true, 411 | oneway: true 412 | }); 413 | } 414 | 415 | function joinwithcamera() { 416 | conversation.peer.peers[targetuser].addStream({ 417 | audio: true, 418 | video: true, 419 | oneway: true 420 | }); 421 | } 422 | 423 | function joinwithscreen() { 424 | conversation.peer.peers[targetuser].addStream({ 425 | screen: true, 426 | oneway: true 427 | }); 428 | } 429 | 430 | conversation.emit('--media-enabled', event.data); 431 | } 432 | 433 | if (data.shareoneway) { 434 | if (data.hasmicrophone) { 435 | if (!user.localstreams.microphone) throw 'You have not allowed microphone.'; 436 | connection.renegotiate(); 437 | } 438 | 439 | if (data.hascamera) { 440 | if (!user.localstreams.camera) throw 'You have not allowed camera.'; 441 | connection.renegotiate(); 442 | } 443 | 444 | if (data.hasscreen) { 445 | if (!user.localstreams.screen) throw 'You have not allowed screen.'; 446 | connection.renegotiate(); 447 | } 448 | } 449 | 450 | if (data.addedfile) { 451 | var filesinfo = data.filesinfo; 452 | if (filesinfo.size && filesinfo.type) { 453 | filesinfo = eventHanlders(filesinfo); 454 | conversation.emit('--add-file', filesinfo); 455 | } else { 456 | for (var file in filesinfo) { 457 | filesinfo[file] = eventHanlders(filesinfo[file]); 458 | conversation.emit('--add-file', filesinfo[file]); 459 | } 460 | } 461 | 462 | function eventHanlders(file) { 463 | file.sender = targetuser; 464 | 465 | file.download = function() { 466 | conversation.peer.send({ 467 | signalingmessage: true, 468 | download: true, 469 | sender: user.username, 470 | file: { 471 | size: file.size, 472 | type: file.type, 473 | name: file.name, 474 | lastModifiedDate: file.lastModifiedDate, 475 | uuid: file.uuid 476 | } 477 | }); 478 | }; 479 | 480 | file.cancel = function() { 481 | conversation.peer.send({ 482 | signalingmessage: true, 483 | download: false, 484 | sender: user.username, 485 | file: { 486 | size: file.size, 487 | type: file.type, 488 | name: file.name, 489 | lastModifiedDate: file.lastModifiedDate, 490 | uuid: file.uuid 491 | } 492 | }); 493 | }; 494 | 495 | return file; 496 | } 497 | } 498 | 499 | if (data.file) { 500 | if (data.download) { 501 | var file = conversation.attachie.files[data.file.name]; 502 | if ((file.item && file.length && file[0] && file[0].lastModifiedDate) || file.forEach) { 503 | Array.prototype.slice.call(file).forEach(function(f) { 504 | conversation.peer.send(f); 505 | }); 506 | } else conversation.peer.send(file); 507 | } else { 508 | var file = conversation.attachie.files[data.file.name]; 509 | conversation.emit('--file-cancelled', file); 510 | } 511 | } 512 | } else { 513 | event.username = conversation.targetuser; 514 | conversation.emit('--message', event); 515 | } 516 | }; 517 | 518 | // overriding "openSignalingChannel" method 519 | connection.openSignalingChannel = function(config) { 520 | var channel = config.channel || this.channel; 521 | 522 | user.onmessagecallbacks[channel] = config.onmessage; 523 | 524 | if (config.onopen) setTimeout(config.onopen, 1000); 525 | 526 | // directly returning socket object using "return" statement 527 | return { 528 | send: function(message) { 529 | user.emit('signal', { 530 | message: message, 531 | isrtcmulticonnectioninnermessage: true, 532 | channel: channel 533 | }); 534 | }, 535 | channel: channel 536 | }; 537 | }; 538 | 539 | // todo: renegotiation doesn't work if trickleIce=false 540 | // need to fix it. 541 | // connection.trickleIce = false; 542 | 543 | connection.sdpConstraints.mandatory = { 544 | OfferToReceiveAudio: true, 545 | OfferToReceiveVideo: true 546 | }; 547 | 548 | connection.autoSaveToDisk = false; 549 | 550 | connection.onFileEnd = function(file) { 551 | if (conversation.attachie.files[file.name]) { 552 | conversation.emit('--file-sent', file); 553 | return; 554 | } 555 | 556 | file.savetodisk = function(filename) { 557 | connection.saveToDisk(file, filename || file.name || file.type); 558 | }; 559 | 560 | file.sender = file.userid; 561 | 562 | conversation.emit('--file-downloaded', file); 563 | }; 564 | 565 | connection.onFileProgress = function(chunk) { 566 | var progress = { 567 | percentage: Math.round((chunk.currentPosition / chunk.maxChunks) * 100), 568 | uuid: chunk.uuid, 569 | file: chunk, 570 | sender: chunk.userid 571 | }; 572 | 573 | if (progress.percentage > 100) progress.percentage = 100; 574 | 575 | conversation.emit('--file-progress', progress); 576 | }; 577 | 578 | connection.onFileStart = function() {}; 579 | 580 | connection.onstreamid = function(event) { 581 | conversation.emit('--stream-clue', event); 582 | }; 583 | 584 | connection.onstream = function(event) { 585 | conversation.emit('--stream', event); 586 | }; 587 | 588 | connection.onstreamended = function(event) { 589 | conversation.emit('--stream-ended', event); 590 | }; 591 | 592 | if (args.isInitiator) { 593 | connection.open({ 594 | dontTransmit: true 595 | }); 596 | } 597 | 598 | user.peers[args.targetuser] = connection; 599 | 600 | user.peers.length++; 601 | connection.onleave = function() { 602 | user.peers.length--; 603 | }; 604 | 605 | user.emit('signal', { 606 | joinRoom: true, 607 | responsefor: args.targetuser, 608 | sessionDescription: connection.sessionDescription 609 | }); 610 | 611 | conversation.peer = connection; 612 | } 613 | 614 | function addfile(file) { 615 | var filesinfo = {}; 616 | 617 | // seems array of files 618 | if ((file.item && file.length && file[0] && file[0].lastModifiedDate) || file.forEach) { 619 | Array.prototype.slice.call(file).forEach(function(f) { 620 | if (!f.uuid) f.uuid = user.randomstring(); 621 | filesinfo[file.name] = { 622 | size: f.size, 623 | type: f.type, 624 | name: f.name, 625 | lastModifiedDate: f.lastModifiedDate, 626 | uuid: f.uuid 627 | }; 628 | conversation.attachie.files[f.name] = f; 629 | }); 630 | } else { 631 | if (!file.uuid) file.uuid = user.randomstring(); 632 | filesinfo[file.name] = { 633 | size: file.size, 634 | type: file.type, 635 | name: file.name, 636 | lastModifiedDate: file.lastModifiedDate, 637 | uuid: file.uuid 638 | }; 639 | 640 | conversation.attachie.files[file.name] = file; 641 | } 642 | 643 | conversation.peer.send({ 644 | addedfile: true, 645 | filesinfo: filesinfo, 646 | signalingmessage: true, 647 | sender: user.username 648 | }); 649 | } 650 | 651 | // custom events 652 | 653 | conversation.events = {}; 654 | conversation.on = function(event, callback) { 655 | conversation.events[event] = callback; 656 | }; 657 | 658 | conversation.emit = function() { 659 | if (!arguments[0]) throw 'At least one argument is mandatory.'; 660 | 661 | var internalevent = arguments[0].indexOf('--') == 0; 662 | if (!internalevent) { 663 | if (arguments[0] == 'message') { 664 | sendmessage(arguments[1]); 665 | } 666 | 667 | if (arguments[0] == 'add-file' && arguments[1]) { 668 | addfile(arguments[1]); 669 | } 670 | 671 | if (arguments[0] == 'enable') { 672 | if (arguments[1] == 'microphone') { 673 | enablemicrophone(); 674 | } 675 | 676 | if (arguments[1] == 'camera') { 677 | enablecamera(); 678 | } 679 | 680 | if (arguments[1] == 'screen') { 681 | enablescreen(); 682 | } 683 | } 684 | 685 | if (arguments[0] == 'end') { 686 | conversation.peer.close(); 687 | user.emit('--ended'); 688 | } 689 | 690 | return; 691 | } 692 | 693 | if (internalevent) { 694 | arguments[0] = arguments[0].replace('--', ''); 695 | } 696 | 697 | if (!conversation.events[arguments[0]]) { 698 | var warning = 'Event name "' + arguments[0] + '" doesn\'t exists.'; 699 | if (arguments[1]) warning += ' Values: ' + JSON.stringify(arguments[1], null, '\t'); 700 | console.warn(warning); 701 | 702 | } else conversation.events[arguments[0]](arguments[1], arguments[2], arguments[3], arguments[4]); 703 | }; 704 | 705 | conversation.on('add-file', function(file) { 706 | file.download(); 707 | }); 708 | 709 | conversation.on('file-downloaded', function(file) { 710 | file.savetodisk(); 711 | }); 712 | 713 | conversation.on('media-enabled', function(media) { 714 | if (media.hasmicrophone) { 715 | media.emit('join-with', 'microphone'); 716 | } 717 | 718 | if (media.hascamera) { 719 | media.emit('join-with', 'camera'); 720 | } 721 | 722 | if (media.hasscreen) { 723 | media.emit('join-with', 'nothing'); 724 | } 725 | }); 726 | 727 | conversation.on('stream-clue', function(stream) { 728 | user.emit('--log', 'stream-clue: ' + stream.streamid); 729 | }); 730 | 731 | conversation.on('stream', function(stream) { 732 | user.emit('--log', 'stream: ' + stream.streamid); 733 | 734 | conversation.peer.body.insertBefore(stream.mediaElement, conversation.peer.body.firstChild); 735 | }); 736 | 737 | conversation.on('stream-ended', function(stream) { 738 | user.emit('--log', 'stream-ended: ' + stream.streamid); 739 | 740 | if (stream.mediaElement && stream.mediaElement.parentNode) { 741 | stream.mediaElement.parentNode.removeChild(stream.mediaElement); 742 | } 743 | }); 744 | 745 | user.conversations[targetuser] = conversation; 746 | } 747 | })(); 748 | -------------------------------------------------------------------------------- /AndroidRTC/scripts/FileBufferReader.js: -------------------------------------------------------------------------------- 1 | // Last time updated at Sep 16, 2014, 08:32:23 2 | 3 | // Latest file can be found here: https://cdn.webrtc-experiment.com/FileBufferReader.js 4 | 5 | // Muaz Khan - www.MuazKhan.com 6 | // MIT License - www.WebRTC-Experiment.com/licence 7 | // Source Code - https://github.com/muaz-khan/FileBufferReader 8 | // Demo - https://www.WebRTC-Experiment.com/FileBufferReader/ 9 | 10 | // ___________________ 11 | // FileBufferReader.js 12 | 13 | // FileBufferReader.js uses binarize.js to convert Objects into array-buffers and vice versa. 14 | // FileBufferReader.js is MIT licensed: www.WebRTC-Experiment.com/licence 15 | // binarize.js is written by @agektmr: https://github.com/agektmr/binarize.js. 16 | 17 | /* issues/features need to be fixed & implemented: 18 | -. "onEnd" for sender now having "url" property as well; same as file receiver. 19 | -. "extra" must not be an empty object i.e. {} -because "binarize" fails to parse empty objects. 20 | -. "extra" must not have "date" types; -because "binarize" fails to parse date-types. 21 | */ 22 | 23 | (function() { 24 | window.FileBufferReader = function() { 25 | var fileBufferReader = this; 26 | fileBufferReader.chunks = {}; 27 | 28 | fileBufferReader.readAsArrayBuffer = function(file, callback, extra) { 29 | extra = extra || {}; 30 | extra.chunkSize = extra.chunkSize || 12 * 1000; // Firefox limit is 16k 31 | 32 | File.Read(file, function(args) { 33 | file.extra = extra || {}; 34 | file.url = URL.createObjectURL(file); 35 | args.file = file; // passed over "onEnd" 36 | fileBufferReader.chunks[args.uuid] = args; 37 | callback(args.uuid); 38 | }, extra); 39 | }; 40 | 41 | fileBufferReader.getNextChunk = function(uuid, callback) { 42 | var chunks = fileBufferReader.chunks[uuid]; 43 | if (!chunks) return; 44 | 45 | var currentChunk = chunks.listOfChunks[chunks.currentPosition]; 46 | var isLastChunk = currentChunk && currentChunk.end; 47 | 48 | FileConverter.ConvertToArrayBuffer(currentChunk, function(buffer) { 49 | if (chunks.currentPosition == 0) { 50 | fileBufferReader.onBegin(chunks.file); 51 | } 52 | 53 | if (isLastChunk) { 54 | fileBufferReader.onEnd(chunks.file); 55 | } 56 | 57 | callback(buffer, isLastChunk, currentChunk.extra); 58 | fileBufferReader.onProgress({ 59 | currentPosition: chunks.currentPosition, 60 | maxChunks: chunks.maxChunks, 61 | uuid: chunks.uuid, 62 | extra: currentChunk.extra 63 | }); 64 | fileBufferReader.chunks[uuid].currentPosition++; 65 | }); 66 | }; 67 | 68 | fileBufferReader.onBegin = fileBufferReader.onProgress = fileBufferReader.onEnd = function() {}; 69 | 70 | var receiver = new File.Receiver(fileBufferReader); 71 | 72 | fileBufferReader.addChunk = function(chunk, callback) { 73 | receiver.receive(chunk, function(uuid) { 74 | FileConverter.ConvertToArrayBuffer({ 75 | readyForNextChunk: true, 76 | uuid: uuid 77 | }, callback); 78 | }); 79 | }; 80 | 81 | fileBufferReader.convertToObject = FileConverter.ConvertToObject; 82 | }; 83 | 84 | window.FileSelector = function() { 85 | var selector = this; 86 | 87 | selector.selectSingleFile = selectFile; 88 | selector.selectMultipleFiles = function(callback) { 89 | selectFile(callback, true); 90 | }; 91 | 92 | function selectFile(callback, multiple) { 93 | var file = document.createElement('input'); 94 | file.type = 'file'; 95 | 96 | if (multiple) { 97 | file.multiple = true; 98 | } 99 | 100 | file.onchange = function() { 101 | callback(multiple ? file.files : file.files[0]); 102 | }; 103 | fireClickEvent(file); 104 | } 105 | 106 | function fireClickEvent(element) { 107 | var evt = new window.MouseEvent('click', { 108 | view: window, 109 | bubbles: true, 110 | cancelable: true 111 | }); 112 | 113 | element.dispatchEvent(evt); 114 | } 115 | }; 116 | 117 | var File = { 118 | Read: function(file, callback, extra) { 119 | var numOfChunksInSlice; 120 | var currentPosition = 1; 121 | var hasEntireFile; 122 | var listOfChunks = {}; 123 | 124 | var chunkSize = extra.chunkSize || 60 * 1000; // 64k max sctp limit (AFAIK!) 125 | 126 | var sliceId = 0; 127 | var cacheSize = chunkSize; 128 | 129 | var chunksPerSlice = Math.floor(Math.min(100000000, cacheSize) / chunkSize); 130 | var sliceSize = chunksPerSlice * chunkSize; 131 | var maxChunks = Math.ceil(file.size / chunkSize); 132 | 133 | // uuid is used to uniquely identify sending instance 134 | var uuid = file.uuid || (Math.random() * new Date().getTime()).toString(36).replace(/\./g, '-'); 135 | 136 | listOfChunks[0] = { 137 | uuid: uuid, 138 | maxChunks: maxChunks, 139 | size: file.size, 140 | name: file.name, 141 | lastModifiedDate: file.lastModifiedDate + '', 142 | type: file.type, 143 | start: true, 144 | extra: extra 145 | }; 146 | 147 | file.maxChunks = maxChunks; 148 | file.uuid = uuid; 149 | 150 | var blob, reader = new FileReader(); 151 | reader.onloadend = function(evt) { 152 | if (evt.target.readyState == FileReader.DONE) { 153 | addChunks(file.name, evt.target.result, function() { 154 | sliceId++; 155 | if ((sliceId + 1) * sliceSize < file.size) { 156 | blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize); 157 | reader.readAsArrayBuffer(blob); 158 | } else if (sliceId * sliceSize < file.size) { 159 | blob = file.slice(sliceId * sliceSize, file.size); 160 | reader.readAsArrayBuffer(blob); 161 | } else { 162 | listOfChunks[currentPosition] = { 163 | uuid: uuid, 164 | maxChunks: maxChunks, 165 | size: file.size, 166 | name: file.name, 167 | lastModifiedDate: file.lastModifiedDate + '', 168 | type: file.type, 169 | end: true, 170 | extra: extra 171 | }; 172 | callback({ 173 | currentPosition: 0, 174 | listOfChunks: listOfChunks, 175 | maxChunks: maxChunks + 1, 176 | uuid: uuid, 177 | extra: extra 178 | }); 179 | } 180 | }); 181 | } 182 | }; 183 | 184 | blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize); 185 | reader.readAsArrayBuffer(blob); 186 | 187 | function addChunks(fileName, binarySlice, callback) { 188 | numOfChunksInSlice = Math.ceil(binarySlice.byteLength / chunkSize); 189 | for (var i = 0; i < numOfChunksInSlice; i++) { 190 | var start = i * chunkSize; 191 | listOfChunks[currentPosition] = { 192 | uuid: uuid, 193 | value: binarySlice.slice(start, Math.min(start + chunkSize, binarySlice.byteLength)), 194 | currentPosition: currentPosition, 195 | maxChunks: maxChunks, 196 | extra: extra 197 | }; 198 | 199 | currentPosition++; 200 | } 201 | 202 | if (currentPosition == maxChunks) { 203 | hasEntireFile = true; 204 | } 205 | 206 | callback(); 207 | } 208 | }, 209 | 210 | Receiver: function(config) { 211 | var packets = {}; 212 | 213 | function receive(chunk, callback) { 214 | if (!chunk.uuid) { 215 | FileConverter.ConvertToObject(chunk, function(object) { 216 | receive(object); 217 | }); 218 | return; 219 | } 220 | 221 | if (chunk.start && !packets[chunk.uuid]) { 222 | packets[chunk.uuid] = []; 223 | if (config.onBegin) config.onBegin(chunk); 224 | } 225 | 226 | if (!chunk.end && chunk.value) { 227 | packets[chunk.uuid].push(chunk.value); 228 | } 229 | 230 | if (chunk.end) { 231 | var _packets = packets[chunk.uuid]; 232 | var finalArray = [], 233 | length = _packets.length; 234 | 235 | for (var i = 0; i < length; i++) { 236 | if (!!_packets[i]) { 237 | finalArray.push(_packets[i]); 238 | } 239 | } 240 | 241 | var blob = new Blob(finalArray, { 242 | type: chunk.type 243 | }); 244 | blob = merge(blob, chunk); 245 | blob.url = URL.createObjectURL(blob); 246 | blob.uuid = chunk.uuid; 247 | 248 | if (!blob.size) console.error('Something went wrong. Blob Size is 0.'); 249 | 250 | if (config.onEnd) config.onEnd(blob); 251 | } 252 | 253 | if (chunk.value && config.onProgress) config.onProgress(chunk); 254 | 255 | if (!chunk.end) callback(chunk.uuid); 256 | } 257 | 258 | return { 259 | receive: receive 260 | }; 261 | }, 262 | SaveToDisk: function(fileUrl, fileName) { 263 | var hyperlink = document.createElement('a'); 264 | hyperlink.href = fileUrl; 265 | hyperlink.target = '_blank'; 266 | hyperlink.download = fileName || fileUrl; 267 | 268 | var mouseEvent = new MouseEvent('click', { 269 | view: window, 270 | bubbles: true, 271 | cancelable: true 272 | }); 273 | 274 | hyperlink.dispatchEvent(mouseEvent); 275 | (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); 276 | } 277 | }; 278 | 279 | // ________________ 280 | // FileConverter.js 281 | var FileConverter = { 282 | ConvertToArrayBuffer: function(object, callback) { 283 | binarize.pack(object, callback); 284 | }, 285 | ConvertToObject: function(buffer, callback) { 286 | binarize.unpack(buffer, callback); 287 | } 288 | }; 289 | 290 | function merge(mergein, mergeto) { 291 | if (!mergein) mergein = {}; 292 | if (!mergeto) return mergein; 293 | 294 | for (var item in mergeto) { 295 | mergein[item] = mergeto[item]; 296 | } 297 | return mergein; 298 | } 299 | 300 | /* 301 | Copyright 2013 Eiji Kitamura 302 | 303 | Licensed under the Apache License, Version 2.0 (the "License"); 304 | you may not use this file except in compliance with the License. 305 | You may obtain a copy of the License at 306 | 307 | http://www.apache.org/licenses/LICENSE-2.0 308 | 309 | Unless required by applicable law or agreed to in writing, software 310 | distributed under the License is distributed on an "AS IS" BASIS, 311 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 312 | See the License for the specific language governing permissions and 313 | limitations under the License. 314 | 315 | Author: Eiji Kitamura (agektmr@gmail.com) 316 | */ 317 | (function(root) { 318 | var debug = false; 319 | 320 | var BIG_ENDIAN = false, 321 | LITTLE_ENDIAN = true, 322 | TYPE_LENGTH = Uint8Array.BYTES_PER_ELEMENT, 323 | LENGTH_LENGTH = Uint16Array.BYTES_PER_ELEMENT, 324 | BYTES_LENGTH = Uint32Array.BYTES_PER_ELEMENT; 325 | 326 | var Types = { 327 | NULL: 0, 328 | UNDEFINED: 1, 329 | STRING: 2, 330 | NUMBER: 3, 331 | BOOLEAN: 4, 332 | ARRAY: 5, 333 | OBJECT: 6, 334 | INT8ARRAY: 7, 335 | INT16ARRAY: 8, 336 | INT32ARRAY: 9, 337 | UINT8ARRAY: 10, 338 | UINT16ARRAY: 11, 339 | UINT32ARRAY: 12, 340 | FLOAT32ARRAY: 13, 341 | FLOAT64ARRAY: 14, 342 | ARRAYBUFFER: 15, 343 | BLOB: 16, 344 | FILE: 16, 345 | BUFFER: 17 // Special type for node.js 346 | }; 347 | 348 | if (debug) { 349 | var TypeNames = [ 350 | 'NULL', 351 | 'UNDEFINED', 352 | 'STRING', 353 | 'NUMBER', 354 | 'BOOLEAN', 355 | 'ARRAY', 356 | 'OBJECT', 357 | 'INT8ARRAY', 358 | 'INT16ARRAY', 359 | 'INT32ARRAY', 360 | 'UINT8ARRAY', 361 | 'UINT16ARRAY', 362 | 'UINT32ARRAY', 363 | 'FLOAT32ARRAY', 364 | 'FLOAT64ARRAY', 365 | 'ARRAYBUFFER', 366 | 'BLOB', 367 | 'BUFFER' 368 | ]; 369 | } 370 | 371 | var Length = [ 372 | null, // Types.NULL 373 | null, // Types.UNDEFINED 374 | 'Uint16', // Types.STRING 375 | 'Float64', // Types.NUMBER 376 | 'Uint8', // Types.BOOLEAN 377 | null, // Types.ARRAY 378 | null, // Types.OBJECT 379 | 'Int8', // Types.INT8ARRAY 380 | 'Int16', // Types.INT16ARRAY 381 | 'Int32', // Types.INT32ARRAY 382 | 'Uint8', // Types.UINT8ARRAY 383 | 'Uint16', // Types.UINT16ARRAY 384 | 'Uint32', // Types.UINT32ARRAY 385 | 'Float32', // Types.FLOAT32ARRAY 386 | 'Float64', // Types.FLOAT64ARRAY 387 | 'Uint8', // Types.ARRAYBUFFER 388 | 'Uint8', // Types.BLOB, Types.FILE 389 | 'Uint8' // Types.BUFFER 390 | ]; 391 | 392 | var binary_dump = function(view, start, length) { 393 | var table = [], 394 | endianness = BIG_ENDIAN, 395 | ROW_LENGTH = 40; 396 | table[0] = []; 397 | for (var i = 0; i < ROW_LENGTH; i++) { 398 | table[0][i] = i < 10 ? '0' + i.toString(10) : i.toString(10); 399 | } 400 | for (i = 0; i < length; i++) { 401 | var code = view.getUint8(start + i, endianness); 402 | var index = ~~(i / ROW_LENGTH) + 1; 403 | if (typeof table[index] === 'undefined') table[index] = []; 404 | table[index][i % ROW_LENGTH] = code < 16 ? '0' + code.toString(16) : code.toString(16); 405 | } 406 | console.log('%c' + table[0].join(' '), 'font-weight: bold;'); 407 | for (i = 1; i < table.length; i++) { 408 | console.log(table[i].join(' ')); 409 | } 410 | }; 411 | 412 | var find_type = function(obj) { 413 | var type = undefined; 414 | 415 | if (obj === undefined) { 416 | type = Types.UNDEFINED; 417 | 418 | } else if (obj === null) { 419 | type = Types.NULL; 420 | 421 | } else { 422 | var const_name = obj.constructor.name; 423 | if (const_name !== undefined) { 424 | // return type by .constructor.name if possible 425 | type = Types[const_name.toUpperCase()]; 426 | 427 | } else { 428 | // Work around when constructor.name is not defined 429 | switch (typeof obj) { 430 | case 'string': 431 | type = Types.STRING; 432 | break; 433 | 434 | case 'number': 435 | type = Types.NUMBER; 436 | break; 437 | 438 | case 'boolean': 439 | type = Types.BOOLEAN; 440 | break; 441 | 442 | case 'object': 443 | if (obj instanceof Array) { 444 | type = Types.ARRAY; 445 | 446 | } else if (obj instanceof Int8Array) { 447 | type = Types.INT8ARRAY; 448 | 449 | } else if (obj instanceof Int16Array) { 450 | type = Types.INT16ARRAY; 451 | 452 | } else if (obj instanceof Int32Array) { 453 | type = Types.INT32ARRAY; 454 | 455 | } else if (obj instanceof Uint8Array) { 456 | type = Types.UINT8ARRAY; 457 | 458 | } else if (obj instanceof Uint16Array) { 459 | type = Types.UINT16ARRAY; 460 | 461 | } else if (obj instanceof Uint32Array) { 462 | type = Types.UINT32ARRAY; 463 | 464 | } else if (obj instanceof Float32Array) { 465 | type = Types.FLOAT32ARRAY; 466 | 467 | } else if (obj instanceof Float64Array) { 468 | type = Types.FLOAT64ARRAY; 469 | 470 | } else if (obj instanceof ArrayBuffer) { 471 | type = Types.ARRAYBUFFER; 472 | 473 | } else if (obj instanceof Blob) { // including File 474 | type = Types.BLOB; 475 | 476 | } else if (obj instanceof Buffer) { // node.js only 477 | type = Types.BUFFER; 478 | 479 | } else if (obj instanceof Object) { 480 | type = Types.OBJECT; 481 | 482 | } 483 | break; 484 | 485 | default: 486 | break; 487 | } 488 | } 489 | } 490 | return type; 491 | }; 492 | 493 | var utf16_utf8 = function(string) { 494 | return unescape(encodeURIComponent(string)); 495 | }; 496 | 497 | var utf8_utf16 = function(bytes) { 498 | return decodeURIComponent(escape(bytes)); 499 | }; 500 | 501 | /** 502 | * packs seriarized elements array into a packed ArrayBuffer 503 | * @param {Array} serialized Serialized array of elements. 504 | * @return {DataView} view of packed binary 505 | */ 506 | var pack = function(serialized) { 507 | var cursor = 0, 508 | i = 0, 509 | j = 0, 510 | endianness = BIG_ENDIAN; 511 | 512 | var ab = new ArrayBuffer(serialized[0].byte_length + serialized[0].header_size); 513 | var view = new DataView(ab); 514 | 515 | for (i = 0; i < serialized.length; i++) { 516 | var start = cursor, 517 | header_size = serialized[i].header_size, 518 | type = serialized[i].type, 519 | length = serialized[i].length, 520 | value = serialized[i].value, 521 | byte_length = serialized[i].byte_length, 522 | type_name = Length[type], 523 | unit = type_name === null ? 0 : root[type_name + 'Array'].BYTES_PER_ELEMENT; 524 | 525 | // Set type 526 | if (type === Types.BUFFER) { 527 | // on node.js Blob is emulated using Buffer type 528 | view.setUint8(cursor, Types.BLOB, endianness); 529 | } else { 530 | view.setUint8(cursor, type, endianness); 531 | } 532 | cursor += TYPE_LENGTH; 533 | 534 | if (debug) { 535 | console.info('Packing', type, TypeNames[type]); 536 | } 537 | 538 | // Set length if required 539 | if (type === Types.ARRAY || type === Types.OBJECT) { 540 | view.setUint16(cursor, length, endianness); 541 | cursor += LENGTH_LENGTH; 542 | 543 | if (debug) { 544 | console.info('Content Length', length); 545 | } 546 | } 547 | 548 | // Set byte length 549 | view.setUint32(cursor, byte_length, endianness); 550 | cursor += BYTES_LENGTH; 551 | 552 | if (debug) { 553 | console.info('Header Size', header_size, 'bytes'); 554 | console.info('Byte Length', byte_length, 'bytes'); 555 | } 556 | 557 | switch (type) { 558 | case Types.NULL: 559 | case Types.UNDEFINED: 560 | // NULL and UNDEFINED doesn't have any payload 561 | break; 562 | 563 | case Types.STRING: 564 | if (debug) { 565 | console.info('Actual Content %c"' + value + '"', 'font-weight:bold;'); 566 | } 567 | for (j = 0; j < length; j++, cursor += unit) { 568 | view.setUint16(cursor, value.charCodeAt(j), endianness); 569 | } 570 | break; 571 | 572 | case Types.NUMBER: 573 | case Types.BOOLEAN: 574 | if (debug) { 575 | console.info('%c' + value.toString(), 'font-weight:bold;'); 576 | } 577 | view['set' + type_name](cursor, value, endianness); 578 | cursor += unit; 579 | break; 580 | 581 | case Types.INT8ARRAY: 582 | case Types.INT16ARRAY: 583 | case Types.INT32ARRAY: 584 | case Types.UINT8ARRAY: 585 | case Types.UINT16ARRAY: 586 | case Types.UINT32ARRAY: 587 | case Types.FLOAT32ARRAY: 588 | case Types.FLOAT64ARRAY: 589 | var _view = new Uint8Array(view.buffer, cursor, byte_length); 590 | _view.set(new Uint8Array(value.buffer)); 591 | cursor += byte_length; 592 | break; 593 | 594 | case Types.ARRAYBUFFER: 595 | case Types.BUFFER: 596 | var _view = new Uint8Array(view.buffer, cursor, byte_length); 597 | _view.set(new Uint8Array(value)); 598 | cursor += byte_length; 599 | break; 600 | 601 | case Types.BLOB: 602 | case Types.ARRAY: 603 | case Types.OBJECT: 604 | break; 605 | 606 | default: 607 | throw 'TypeError: Unexpected type found.'; 608 | } 609 | 610 | if (debug) { 611 | binary_dump(view, start, cursor - start); 612 | } 613 | } 614 | 615 | return view; 616 | }; 617 | 618 | /** 619 | * Unpack binary data into an object with value and cursor 620 | * @param {DataView} view [description] 621 | * @param {Number} cursor [description] 622 | * @return {Object} 623 | */ 624 | var unpack = function(view, cursor) { 625 | var i = 0, 626 | endianness = BIG_ENDIAN, 627 | start = cursor; 628 | var type, length, byte_length, value, elem; 629 | 630 | // Retrieve "type" 631 | type = view.getUint8(cursor, endianness); 632 | cursor += TYPE_LENGTH; 633 | 634 | if (debug) { 635 | console.info('Unpacking', type, TypeNames[type]); 636 | } 637 | 638 | // Retrieve "length" 639 | if (type === Types.ARRAY || type === Types.OBJECT) { 640 | length = view.getUint16(cursor, endianness); 641 | cursor += LENGTH_LENGTH; 642 | 643 | if (debug) { 644 | console.info('Content Length', length); 645 | } 646 | } 647 | 648 | // Retrieve "byte_length" 649 | byte_length = view.getUint32(cursor, endianness); 650 | cursor += BYTES_LENGTH; 651 | 652 | if (debug) { 653 | console.info('Byte Length', byte_length, 'bytes'); 654 | } 655 | 656 | var type_name = Length[type]; 657 | var unit = type_name === null ? 0 : root[type_name + 'Array'].BYTES_PER_ELEMENT; 658 | 659 | switch (type) { 660 | case Types.NULL: 661 | case Types.UNDEFINED: 662 | if (debug) { 663 | binary_dump(view, start, cursor - start); 664 | } 665 | // NULL and UNDEFINED doesn't have any octet 666 | value = null; 667 | break; 668 | 669 | case Types.STRING: 670 | length = byte_length / unit; 671 | var string = []; 672 | for (i = 0; i < length; i++) { 673 | var code = view.getUint16(cursor, endianness); 674 | cursor += unit; 675 | string.push(String.fromCharCode(code)); 676 | } 677 | value = string.join(''); 678 | if (debug) { 679 | console.info('Actual Content %c"' + value + '"', 'font-weight:bold;'); 680 | binary_dump(view, start, cursor - start); 681 | } 682 | break; 683 | 684 | case Types.NUMBER: 685 | value = view.getFloat64(cursor, endianness); 686 | cursor += unit; 687 | if (debug) { 688 | console.info('Actual Content %c"' + value.toString() + '"', 'font-weight:bold;'); 689 | binary_dump(view, start, cursor - start); 690 | } 691 | break; 692 | 693 | case Types.BOOLEAN: 694 | value = view.getUint8(cursor, endianness) === 1 ? true : false; 695 | cursor += unit; 696 | if (debug) { 697 | console.info('Actual Content %c"' + value.toString() + '"', 'font-weight:bold;'); 698 | binary_dump(view, start, cursor - start); 699 | } 700 | break; 701 | 702 | case Types.INT8ARRAY: 703 | case Types.INT16ARRAY: 704 | case Types.INT32ARRAY: 705 | case Types.UINT8ARRAY: 706 | case Types.UINT16ARRAY: 707 | case Types.UINT32ARRAY: 708 | case Types.FLOAT32ARRAY: 709 | case Types.FLOAT64ARRAY: 710 | case Types.ARRAYBUFFER: 711 | elem = view.buffer.slice(cursor, cursor + byte_length); 712 | cursor += byte_length; 713 | 714 | // If ArrayBuffer 715 | if (type === Types.ARRAYBUFFER) { 716 | value = elem; 717 | 718 | // If other TypedArray 719 | } else { 720 | value = new root[type_name + 'Array'](elem); 721 | } 722 | 723 | if (debug) { 724 | binary_dump(view, start, cursor - start); 725 | } 726 | break; 727 | 728 | case Types.BLOB: 729 | if (debug) { 730 | binary_dump(view, start, cursor - start); 731 | } 732 | // If Blob is available (on browser) 733 | if (root.Blob) { 734 | var mime = unpack(view, cursor); 735 | var buffer = unpack(view, mime.cursor); 736 | cursor = buffer.cursor; 737 | value = new Blob([buffer.value], { 738 | type: mime.value 739 | }); 740 | } else { 741 | // node.js implementation goes here 742 | elem = view.buffer.slice(cursor, cursor + byte_length); 743 | cursor += byte_length; 744 | // node.js implementatino uses Buffer to help Blob 745 | value = new Buffer(elem); 746 | } 747 | break; 748 | 749 | case Types.ARRAY: 750 | if (debug) { 751 | binary_dump(view, start, cursor - start); 752 | } 753 | value = []; 754 | for (i = 0; i < length; i++) { 755 | // Retrieve array element 756 | elem = unpack(view, cursor); 757 | cursor = elem.cursor; 758 | value.push(elem.value); 759 | } 760 | break; 761 | 762 | case Types.OBJECT: 763 | if (debug) { 764 | binary_dump(view, start, cursor - start); 765 | } 766 | value = {}; 767 | for (i = 0; i < length; i++) { 768 | // Retrieve object key and value in sequence 769 | var key = unpack(view, cursor); 770 | var val = unpack(view, key.cursor); 771 | cursor = val.cursor; 772 | value[key.value] = val.value; 773 | } 774 | break; 775 | 776 | default: 777 | throw 'TypeError: Type not supported.'; 778 | } 779 | return { 780 | value: value, 781 | cursor: cursor 782 | }; 783 | }; 784 | 785 | /** 786 | * deferred function to process multiple serialization in order 787 | * @param {array} array [description] 788 | * @param {Function} callback [description] 789 | * @return {void} no return value 790 | */ 791 | var deferredSerialize = function(array, callback) { 792 | var length = array.length, 793 | results = [], 794 | count = 0, 795 | byte_length = 0; 796 | for (var i = 0; i < array.length; i++) { 797 | (function(index) { 798 | serialize(array[index], function(result) { 799 | // store results in order 800 | results[index] = result; 801 | // count byte length 802 | byte_length += result[0].header_size + result[0].byte_length; 803 | // when all results are on table 804 | if (++count === length) { 805 | // finally concatenate all reuslts into a single array in order 806 | var array = []; 807 | for (var j = 0; j < results.length; j++) { 808 | array = array.concat(results[j]); 809 | } 810 | callback(array, byte_length); 811 | } 812 | }); 813 | })(i); 814 | } 815 | }; 816 | 817 | /** 818 | * Serializes object and return byte_length 819 | * @param {mixed} obj JavaScript object you want to serialize 820 | * @return {Array} Serialized array object 821 | */ 822 | var serialize = function(obj, callback) { 823 | var subarray = [], 824 | unit = 1, 825 | header_size = TYPE_LENGTH + BYTES_LENGTH, 826 | type, byte_length = 0, 827 | length = 0, 828 | value = obj; 829 | 830 | type = find_type(obj); 831 | 832 | unit = Length[type] === undefined || Length[type] === null ? 0 : 833 | root[Length[type] + 'Array'].BYTES_PER_ELEMENT; 834 | 835 | switch (type) { 836 | case Types.UNDEFINED: 837 | case Types.NULL: 838 | break; 839 | 840 | case Types.NUMBER: 841 | case Types.BOOLEAN: 842 | byte_length = unit; 843 | break; 844 | 845 | case Types.STRING: 846 | length = obj.length; 847 | byte_length += length * unit; 848 | break; 849 | 850 | case Types.INT8ARRAY: 851 | case Types.INT16ARRAY: 852 | case Types.INT32ARRAY: 853 | case Types.UINT8ARRAY: 854 | case Types.UINT16ARRAY: 855 | case Types.UINT32ARRAY: 856 | case Types.FLOAT32ARRAY: 857 | case Types.FLOAT64ARRAY: 858 | length = obj.length; 859 | byte_length += length * unit; 860 | break; 861 | 862 | case Types.ARRAY: 863 | deferredSerialize(obj, function(subarray, byte_length) { 864 | callback([{ 865 | type: type, 866 | length: obj.length, 867 | header_size: header_size + LENGTH_LENGTH, 868 | byte_length: byte_length, 869 | value: null 870 | }].concat(subarray)); 871 | }); 872 | return; 873 | 874 | case Types.OBJECT: 875 | var deferred = []; 876 | for (var key in obj) { 877 | if (obj.hasOwnProperty(key)) { 878 | deferred.push(key); 879 | deferred.push(obj[key]); 880 | length++; 881 | } 882 | } 883 | deferredSerialize(deferred, function(subarray, byte_length) { 884 | callback([{ 885 | type: type, 886 | length: length, 887 | header_size: header_size + LENGTH_LENGTH, 888 | byte_length: byte_length, 889 | value: null 890 | }].concat(subarray)); 891 | }); 892 | return; 893 | 894 | case Types.ARRAYBUFFER: 895 | byte_length += obj.byteLength; 896 | break; 897 | 898 | case Types.BLOB: 899 | var mime_type = obj.type; 900 | var reader = new FileReader(); 901 | reader.onload = function(e) { 902 | deferredSerialize([mime_type, e.target.result], function(subarray, byte_length) { 903 | callback([{ 904 | type: type, 905 | length: length, 906 | header_size: header_size, 907 | byte_length: byte_length, 908 | value: null 909 | }].concat(subarray)); 910 | }); 911 | }; 912 | reader.onerror = function(e) { 913 | throw 'FileReader Error: ' + e; 914 | }; 915 | reader.readAsArrayBuffer(obj); 916 | return; 917 | 918 | case Types.BUFFER: 919 | byte_length += obj.length; 920 | break; 921 | 922 | default: 923 | throw 'TypeError: Type "' + obj.constructor.name + '" not supported.'; 924 | } 925 | 926 | callback([{ 927 | type: type, 928 | length: length, 929 | header_size: header_size, 930 | byte_length: byte_length, 931 | value: value 932 | }].concat(subarray)); 933 | }; 934 | 935 | /** 936 | * Deserialize binary and return JavaScript object 937 | * @param ArrayBuffer buffer ArrayBuffer you want to deserialize 938 | * @return mixed Retrieved JavaScript object 939 | */ 940 | var deserialize = function(buffer, callback) { 941 | var view = buffer instanceof DataView ? buffer : new DataView(buffer); 942 | var result = unpack(view, 0); 943 | return result.value; 944 | }; 945 | 946 | if (debug) { 947 | root.Test = { 948 | BIG_ENDIAN: BIG_ENDIAN, 949 | LITTLE_ENDIAN: LITTLE_ENDIAN, 950 | Types: Types, 951 | pack: pack, 952 | unpack: unpack, 953 | serialize: serialize, 954 | deserialize: deserialize 955 | }; 956 | } 957 | 958 | var binarize = { 959 | pack: function(obj, callback) { 960 | try { 961 | if (debug) console.info('%cPacking Start', 'font-weight: bold; color: red;', obj); 962 | serialize(obj, function(array) { 963 | if (debug) console.info('Serialized Object', array); 964 | callback(pack(array)); 965 | }); 966 | } catch (e) { 967 | throw e; 968 | } 969 | }, 970 | unpack: function(buffer, callback) { 971 | try { 972 | if (debug) console.info('%cUnpacking Start', 'font-weight: bold; color: red;', buffer); 973 | var result = deserialize(buffer); 974 | if (debug) console.info('Deserialized Object', result); 975 | callback(result); 976 | } catch (e) { 977 | throw e; 978 | } 979 | } 980 | }; 981 | 982 | if (typeof module !== 'undefined' && module.exports) { 983 | module.exports = binarize; 984 | } else { 985 | root.binarize = binarize; 986 | } 987 | })(typeof global !== 'undefined' ? global : this); 988 | })(); 989 | --------------------------------------------------------------------------------