├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENCE ├── README.md ├── cert ├── README.md ├── server.crt └── server.key ├── chrome ├── README.md ├── background.js ├── content.js ├── icon.png └── manifest.json ├── package.json ├── public ├── index.html ├── observable.js ├── polyfills.js ├── qrcode.js ├── rtcpolyfills.js ├── scripts.js └── styles.css └── server.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | *.log -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:0.12.0-slim 2 | 3 | MAINTAINER Curiosity driven 4 | 5 | WORKDIR /src 6 | COPY package.json /src/ 7 | RUN npm install 8 | 9 | COPY . /src 10 | 11 | ENV PORT 3000 12 | EXPOSE 3000 13 | 14 | CMD ["npm", "start"] 15 | 16 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Curiosity driven 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Reactive WebRTC Conference 2 | ============================= 3 | 4 | Simple WebRTC demo that utilizes Observables. 5 | 6 | See https://curiosity-driven.org/reactive-webrtc-conference for details. 7 | 8 | Local 9 | ----- 10 | 11 | To start locally use: 12 | 13 | npm install 14 | npm start 15 | 16 | To start in a secure (HTTPS) mode (required for the Screensharing plugin): 17 | 18 | npm start --secure 19 | 20 | Heroku 21 | ------ 22 | 23 | To deploy on Heroku: 24 | 25 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 26 | 27 | Docker 28 | ------ 29 | 30 | To run inside a Docker container: 31 | 32 | docker build -t webrtcconf:1 . 33 | docker run --rm -it -p 3000:3000 webrtcconf:1 34 | 35 | -------------------------------------------------------------------------------- /cert/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -sha256 -subj "/CN=Curiosity driven" -keyout server.key -out server.crt 4 | -------------------------------------------------------------------------------- /cert/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDNTCCAh2gAwIBAgIJAKaJNu0eKhs0MA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV 3 | BAMTEEN1cmlvc2l0eSBkcml2ZW4wHhcNMTUwMzA1MTIzOTQ5WhcNMjUwMzAyMTIz 4 | OTQ5WjAbMRkwFwYDVQQDExBDdXJpb3NpdHkgZHJpdmVuMIIBIjANBgkqhkiG9w0B 5 | AQEFAAOCAQ8AMIIBCgKCAQEA6wiiV3DBbbcjEpim1IJ1uJevrwhC/L5/XS5YjMGm 6 | HqdQBg7cbflgh6uj6UkicMhboU6XDYcqh5KZmlCXfZnH8EvAbT/fFUEb9zV6bHxI 7 | w68cMfMiJgIS/H9Odgq7lPXwMTIyHH0+QMgMCtIsrjVL3YD5caBpKnMlj8RvKJIA 8 | dK8MhHGX7u1c4UZ8ZjEWQGge+pNZt3COqegua5nnqsoo6q4t8n1+y26bdEq5lgO1 9 | dazmKfEpWijwQ/3ZSo43LAGDxUWrlKHUxBFRZjdO8WKTXHAiQOjvCZyWQ8fuZDe4 10 | +7XNwGa5CDHhrTxhA63/a/bIrI8wWs0mHxTBP+faSD/jYwIDAQABo3wwejAdBgNV 11 | HQ4EFgQUzGS0aDXxpqn+2oPKU9jkH6d+cMkwSwYDVR0jBEQwQoAUzGS0aDXxpqn+ 12 | 2oPKU9jkH6d+cMmhH6QdMBsxGTAXBgNVBAMTEEN1cmlvc2l0eSBkcml2ZW6CCQCm 13 | iTbtHiobNDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAcNrd5zK4T 14 | 4m8OK8+rLNZwtpsYUynQoD9NMSvHb7yhl9S9iGdReuXv5MML2MqsipxgHijlhQ3a 15 | emuIC9Cz+rZZDvAmHEmpRfc9hUyS1hLGF3Au7FDMvhXdB8aqniPJtZwcUIiNxNjD 16 | YT2ai8LLRx0nF2fflJYu90LEPZsdiT/y0JLUYJB3VO7Yi61rXke8cwnFv+FGqlqE 17 | T4Ehio+syyhNJjxHLVcq9V7+wkQz8mFj0rkwVbZDYN/ayu5FHAtLe2euhllxNmLZ 18 | XTlVp0ItNhHZmLmhZhWx6BwXlrU9PKH/r74JHm2cHO5IdYA7LAtLNvOZsIyxcxdM 19 | QAZtAgCfmJ5j 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /cert/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA6wiiV3DBbbcjEpim1IJ1uJevrwhC/L5/XS5YjMGmHqdQBg7c 3 | bflgh6uj6UkicMhboU6XDYcqh5KZmlCXfZnH8EvAbT/fFUEb9zV6bHxIw68cMfMi 4 | JgIS/H9Odgq7lPXwMTIyHH0+QMgMCtIsrjVL3YD5caBpKnMlj8RvKJIAdK8MhHGX 5 | 7u1c4UZ8ZjEWQGge+pNZt3COqegua5nnqsoo6q4t8n1+y26bdEq5lgO1dazmKfEp 6 | WijwQ/3ZSo43LAGDxUWrlKHUxBFRZjdO8WKTXHAiQOjvCZyWQ8fuZDe4+7XNwGa5 7 | CDHhrTxhA63/a/bIrI8wWs0mHxTBP+faSD/jYwIDAQABAoIBAG2vrNuz4oGKe1K9 8 | bPY2ICxSlgnQiDqdyKC69VZTNWyO1rjNDLBCNnt6bdd8axdJWgHwxGvqzb1RfF/Z 9 | Bqn2L6oEIDycF0c7CIsZKRYh9m9kdHsXJbOpOiUeYIaUQbCmCj9bVqmXBYuEEKKo 10 | LXPrqYkpHMCbhRjrffcXTG0Znh7Vgb+juI1WnmYxDVIi4gj3Z7sIz0QM0lbwD6gO 11 | KhXJZohwJDayxWn4KZAWD9b57PU7ExaAqCCDRsgfpSFYq2eULQLE/bFUpD2t8lQn 12 | JqLL4xl2qHpIFfAHQU3l6wrEr6esBDeg36tBt3OWE57v09hEKAmcXwkcXdP60+O4 13 | v3rbSfECgYEA+L+CF56M2hrf0r/RDzcLRHj0vILf3/LqMskpcMEKDdUj67HyMtdG 14 | EGYIgt9oWMe7j5BCqtpEUhm3V/2mUJnqrazTLBDggm6MdF3DQGOfv4JnPTQbPkAT 15 | Uspv1kOdnHe+lzqM9I5xKqJkZ0ISR5qKeVB05RibJZ+WODgpnimH1VsCgYEA8eLF 16 | Zd9NvSZYCSsIB/EaOLmcc7uKehou2eB66MOJaawpvM8HW4yoNUbTuG7cNthhAnvz 17 | /otQm0p0dphY7svhR4T3PojmZX2VzfDcwqmP6975ovF+1Xj2ve1xsu1Jfsk5BzOB 18 | M2dV1uWACZWE5QHtisOyOzWC3+B04rhFmmESIJkCgYEAgx7gAndPJAEajssR9oU7 19 | aUKhL2WFgVVY4qBrOcZn+Far8qgAVZBonGhMgEAnjvTqB4kxu0IG1Yg8vyMzsjUQ 20 | IbCCOC5FSjvfyc9LBvv8z0R8CyUWX0ADb5bKURWfVUVBsBHrD1aujJzBdDS90gTC 21 | jaQ9mi3YSoLO+p+QQJD/yxUCgYAs1/PsvQd2h8NOf9HPVx2bYp3kvuIfXPdOoVVo 22 | DchN9QMP4/njOSJ+LhFWYgcli9wAT+aeTEm3YIhS7E+ghd/QGJCV4V+FdVDuizcC 23 | R9lMy1vQn6D6BqEH+RtZJrC3dqrB8QSE+SVq38VrAXNP2ZmmXj2OyI7o3n4NAki1 24 | JEXuEQKBgB3ZaOxOomR/HEEKiktu9XvbPCrOoEpkAr/X85Tqz14Ru0mhkhVxVA29 25 | SP4B0nbiQIlIo0LS4X3BqBS7i4YnHz5pdkpahir1i4P80HZWMuOQTQMC3jh4BSvP 26 | wpkE+J6wCmk9g2Pa8A0JoiVbCVikHGWml990nYyU2WGdoXCyeIGE 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /chrome/README.md: -------------------------------------------------------------------------------- 1 | Chrome Screen capturing extension 2 | ================================= 3 | 4 | Allows selecting screens and windows as a WebRTC source in Chrome. 5 | 6 | Drag and drop this folder onto `chrome://extensions/` page to turn this extension 7 | on in developer mode. 8 | 9 | Then start server in secure mode: 10 | 11 | npm start --secure 12 | 13 | -------------------------------------------------------------------------------- /chrome/background.js: -------------------------------------------------------------------------------- 1 |  2 | chrome.runtime.onConnect.addListener(function (port) { 3 | port.onMessage.addListener(onMessage.bind(port)); 4 | }); 5 | 6 | function onMessage(message) { 7 | if (message.command == 'get-sourceid' && !message.type) { 8 | chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], 9 | // tab is required so that the sourceId can be used there 10 | this.sender.tab, 11 | onStream.bind(this, message.id)); 12 | } else if (message.command == 'check-installed' && !message.type) { 13 | this.postMessage({ 14 | id: message.id, 15 | command: 'check-installed', 16 | type: 'result' 17 | }); 18 | } 19 | } 20 | function onStream(id, streamId) { 21 | this.postMessage({ 22 | command: 'get-sourceid', 23 | type: 'result', 24 | id: id, 25 | streamId: streamId 26 | }); 27 | } 28 | 29 | chrome.runtime.onInstalled.addListener(injectContentScript); 30 | 31 | function injectContentScript() { 32 | var contentScript = chrome.runtime.getManifest().content_scripts[0]; 33 | chrome.tabs.query({ url: contentScript.matches }, function(tabs) { 34 | tabs.forEach(function(tab) { 35 | chrome.tabs.executeScript(tab.id, { file: contentScript.js[0] }); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /chrome/content.js: -------------------------------------------------------------------------------- 1 | var port = chrome.runtime.connect(); 2 | 3 | port.onMessage.addListener(function (message) { 4 | window.postMessage(message, '*'); 5 | }); 6 | 7 | window.addEventListener('message', function (event) { 8 | port.postMessage(event.data); 9 | }); 10 | -------------------------------------------------------------------------------- /chrome/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curiosity-driven/reactive-webrtc-conference/d53586ba0af14882bcfc049a8d6e858ef6c71f13/chrome/icon.png -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version" : 2, 3 | "name" : "Screen capturing", 4 | "author": "Curiosity driven ", 5 | "version" : "42.4", 6 | "description" : "Enable screen sharing in WebRTC conferences.", 7 | "homepage_url": "https://curiosity-driven.org/reactive-webrtc-conference", 8 | "background": { 9 | "scripts": ["background.js"], 10 | "persistent": false 11 | }, 12 | "content_scripts": [ { 13 | "js": [ "content.js" ], 14 | "run_at": "document_start", 15 | "matches": ["*://docker/*", "*://localhost/*", "https://curiosity-driven.org/*"] 16 | }], 17 | "icons" : { 18 | "128" : "icon.png" 19 | }, 20 | "permissions": [ 21 | "desktopCapture", 22 | "*://docker/*", 23 | "*://localhost/*", 24 | "https://curiosity-driven.org/*" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conference-server", 3 | "version": "1.0.0", 4 | "description": "Simple chat room web socket server", 5 | "author": "Curiosity driven ", 6 | "license": "APL", 7 | "dependencies": { 8 | "express": "^4.11.2", 9 | "ws": "^0.7.1" 10 | }, 11 | "engines": { 12 | "node": ">=0.10.x" 13 | }, 14 | "config": { 15 | "port": 3000 16 | }, 17 | "scripts": { 18 | "docker-run": "docker build . | grep 'Successfully built' | cut -d' ' -f3 | xargs docker run -i --rm -p 3000:3000" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | Conference 20 | 21 | 22 | 23 | 24 |
25 |
26 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/observable.js: -------------------------------------------------------------------------------- 1 | function decorate(iterator, onDone) { 2 | var done = false, 3 | nextFn = iterator.next; 4 | 5 | return Object.create( 6 | iterator, 7 | { 8 | throw: { 9 | value: function(e) { 10 | var throwFn = iterator.throw; 11 | if (!done) { 12 | done = true; 13 | if (onDone) { 14 | onDone.call(this); 15 | } 16 | 17 | if (throwFn) { 18 | return throwFn.call(iterator, e); 19 | } 20 | } 21 | } 22 | }, 23 | return: { 24 | value: function(v) { 25 | var returnFn = iterator.return; 26 | if (!done) { 27 | done = true; 28 | if (onDone) { 29 | onDone.call(this); 30 | } 31 | 32 | if (returnFn) { 33 | return returnFn.call(iterator, v); 34 | } 35 | } 36 | } 37 | }, 38 | }); 39 | }; 40 | 41 | var microTaskScheduler = function(fn, args) { 42 | //asap(fn.bind(null, args)); 43 | setTimeout(function() { 44 | fn(args); 45 | }) 46 | }; 47 | 48 | function Observable(observeDefn) { 49 | this.observe = observeDefn; 50 | } 51 | 52 | Observable.fromEventPattern = function(add, remove, scheduler) { 53 | scheduler = scheduler || microTaskScheduler; 54 | 55 | return new Observable(function observe(generator) { 56 | var handler, 57 | decoratedGenerator = 58 | decorate( 59 | generator, 60 | function() { 61 | remove(handler); 62 | }), 63 | next = decoratedGenerator.next; 64 | 65 | handler = function() { 66 | if (next) { 67 | next.apply(decoratedGenerator, Array.prototype.slice.call(arguments)); 68 | } 69 | }; 70 | 71 | scheduler(function() { add(handler) }); 72 | 73 | return decoratedGenerator; 74 | }); 75 | }; 76 | 77 | // Convert any DOM event into an async generator 78 | Observable.fromEvent = function(dom, eventName, syncAction, scheduler) { 79 | scheduler = scheduler || microTaskScheduler; 80 | 81 | return new Observable(function fromDOMEventObserve(generator) { 82 | var handler, 83 | decoratedGenerator = 84 | decorate( 85 | generator, 86 | function onDone() { 87 | dom.removeEventListener(eventName, handler); 88 | }); 89 | 90 | handler = function(e) { 91 | if (syncAction) { 92 | syncAction(e); 93 | } 94 | 95 | decoratedGenerator.next(e); 96 | }; 97 | 98 | scheduler(function() { 99 | dom.addEventListener(eventName, handler) 100 | }); 101 | 102 | return decoratedGenerator; 103 | }); 104 | }; 105 | 106 | Observable.empty = function(scheduler) { 107 | scheduler = scheduler || microTaskScheduler; 108 | return new Observable(function(generator) { 109 | var done = false, 110 | decoratedGenerator = decorate(generator); 111 | 112 | scheduler(decoratedGenerator.return.bind(decoratedGenerator)); 113 | 114 | return decoratedGenerator; 115 | }); 116 | }; 117 | 118 | Observable.from = function(arr, scheduler) { 119 | scheduler = scheduler || microTaskScheduler; 120 | 121 | return new Observable(function(generator) { 122 | var done = false, 123 | decoratedGenerator = 124 | decorate(generator, function() { done = true }); 125 | 126 | scheduler(function() { 127 | for(var count = 0; count < arr.length; count++) { 128 | if (done) { 129 | return; 130 | } 131 | decoratedGenerator.next(arr[count]); 132 | } 133 | if (done) { 134 | return; 135 | } 136 | decoratedGenerator.return(); 137 | }); 138 | 139 | return decoratedGenerator; 140 | }) 141 | }; 142 | 143 | Observable.merge = function() { 144 | return Observable.from(Array.prototype.slice.call(arguments)).mergeAll(); 145 | } 146 | 147 | Observable.concat = function() { 148 | return Observable.from(Array.prototype.slice.call(arguments)).concatAll(); 149 | } 150 | 151 | Observable.of = function() { 152 | return Observable.from(Array.prototype.slice.call(arguments)); 153 | }; 154 | 155 | Observable.interval = function(time) { 156 | return new Observable(function forEach(generator) { 157 | var handle, 158 | decoratedGenerator = decorate(generator, function() { clearInterval(handle); }); 159 | 160 | handle = setInterval(function() { 161 | decoratedGenerator.next(); 162 | }, time); 163 | 164 | return decoratedGenerator; 165 | }); 166 | }; 167 | 168 | Observable.timeout = function(time) { 169 | return new Observable(function forEach(observer) { 170 | var handle, 171 | decoratedObserver = decorate(observer, function() { clearInterval(handle); }); 172 | 173 | handle = setTimeout(function() { 174 | decoratedObserver.next(); 175 | decoratedObserver.return(); 176 | }, time); 177 | 178 | return decoratedObserver; 179 | }); 180 | }; 181 | 182 | Observable.prototype = { 183 | lift: function(generatorTransform) { 184 | var self = this; 185 | return new Observable(function(generator) { 186 | return self.observe(generatorTransform.call(this, generator)); 187 | }); 188 | }, 189 | map: function(projection, thisArg) { 190 | var index = 0; 191 | return this.lift( 192 | function(generator) { 193 | thisArg = thisArg !== undefined ? thisArg : this; 194 | return Object.create( 195 | generator, 196 | { 197 | next: { 198 | value: function(value) { 199 | var next = generator.next; 200 | if (next) { 201 | try { 202 | return next.call(generator, projection.call(thisArg, value), index++, this); 203 | } 204 | catch(e) { 205 | return this.throw(e); 206 | } 207 | } 208 | } 209 | } 210 | }) 211 | }); 212 | }, 213 | filter: function(predicate, thisArg) { 214 | return this.lift( 215 | function(generator) { 216 | thisArg = thisArg !== undefined ? thisArg : this; 217 | return Object.create( 218 | generator, 219 | { 220 | next: { 221 | value: function(value) { 222 | var next = generator.next, 223 | throwFn; 224 | 225 | if (next && predicate.call(thisArg, value)) { 226 | try { 227 | return next.call(generator, value); 228 | } 229 | catch(e) { 230 | throwFn = this.throw; 231 | if (throwFn) { 232 | throwFn.call(this, e); 233 | } 234 | } 235 | } 236 | } 237 | } 238 | }) 239 | }); 240 | }, 241 | scan: function(combiner, acc) { 242 | return this.lift( 243 | function(generator) { 244 | var next = generator.next, 245 | returnFn = generator.return, 246 | index = 0, 247 | self = this; 248 | 249 | return Object.create( 250 | generator, 251 | { 252 | next: { 253 | value: function(value) { 254 | if (initialValue === undefined) { 255 | acc = value; 256 | } 257 | else if (next && predicate(value)) { 258 | return next.call(generator, combiner.call(null, acc, value, index++, self)); 259 | } 260 | } 261 | }, 262 | return: { 263 | value: function(value) { 264 | if (next) { 265 | next.call(generator, acc); 266 | } 267 | if (returnFn) { 268 | return returnFn.call(generator, value); 269 | } 270 | } 271 | } 272 | }) 273 | }); 274 | }, 275 | reverse: function() { 276 | return this. 277 | toArray(). 278 | mergeMap(function(arr) { 279 | return Observable.from(arr.reverse()); 280 | }); 281 | }, 282 | toArray: function() { 283 | return this. 284 | reduce(function(acc, cur) { 285 | acc.push(cur); 286 | return acc; 287 | }, []) 288 | }, 289 | first: function() { 290 | return this.lift( 291 | function(generator) { 292 | var next = generator.next, 293 | returnFn = generator.return; 294 | 295 | return Object.create( 296 | generator, 297 | { 298 | next: { 299 | value: function(value) { 300 | if (next) { 301 | next.call(generator, value); 302 | } 303 | this.return(); 304 | } 305 | } 306 | }) 307 | }); 308 | }, 309 | last: function() { 310 | var lastValue; 311 | return this.lift( 312 | function(generator) { 313 | var next = generator.next, 314 | returnFn = generator.return; 315 | 316 | return Object.create( 317 | generator, 318 | { 319 | next: { 320 | value: function(value) { 321 | lastValue = value; 322 | } 323 | }, 324 | return: { 325 | value: function(value) { 326 | if (next && lastValue !== undefined) { 327 | next.call(generator, lastValue); 328 | } 329 | if (returnFn) { 330 | return returnFn.call(generator, value); 331 | } 332 | } 333 | } 334 | }) 335 | }); 336 | }, 337 | reduce: function(combiner, acc) { 338 | return this.scan(combiner, acc).last(); 339 | }, 340 | find: function(predicate, thisArg) { 341 | return this.filter(predicate, thisArg).first(); 342 | }, 343 | skip: function(num) { 344 | return this.lift( 345 | function(generator) { 346 | var next = generator.next, 347 | returnFn = generator.return; 348 | 349 | return Object.create( 350 | generator, 351 | { 352 | next: { 353 | value: function(value) { 354 | num--; 355 | if (num < 0 && next) { 356 | next.call(generator,value); 357 | } 358 | } 359 | } 360 | }) 361 | }); 362 | }, 363 | forEach: function(next) { 364 | var self = this; 365 | return new Promise(function(resolve, reject) { 366 | self.observe({ 367 | next: next, 368 | return: function() { 369 | resolve(); 370 | }, 371 | throw: reject 372 | }); 373 | }); 374 | }, 375 | done: function() { 376 | var self = this; 377 | return new Promise(function(resolve, reject) { 378 | self.observe({ 379 | next: function(){}, 380 | return: resolve, 381 | throw: reject 382 | }); 383 | }); 384 | }, 385 | take: function(num) { 386 | var self = this, 387 | count = 0; 388 | 389 | return this.lift( 390 | function(generator) { 391 | var next = generator.next; 392 | return Object.create( 393 | generator, 394 | { 395 | next: { 396 | value: function(value) { 397 | var result = next.call(generator, value); 398 | 399 | if (count === num - 1) { 400 | result = this.return(); 401 | } 402 | 403 | count++; 404 | return result; 405 | } 406 | } 407 | }); 408 | }); 409 | }, 410 | takeUntil: function(stops) { 411 | var self = this; 412 | return new Observable(function(generator) { 413 | var next = generator.next, 414 | throwFn = generator.throw, 415 | returnFn = generator.return, 416 | decoratedGenerator, 417 | stopGenerator = 418 | stops.observe({ 419 | done: false, 420 | next: function(v) { 421 | stopGenerator.return(); 422 | return decoratedGenerator.return(); 423 | }, 424 | throw: function(e) { 425 | return decoratedGenerator.throw(e); 426 | }, 427 | return: function(v) { 428 | return decoratedGenerator.return(); 429 | } 430 | }); 431 | 432 | decoratedGenerator = 433 | self.observe( 434 | Object.create( 435 | generator, 436 | { 437 | throw: { 438 | value: function(e) { 439 | stopGenerator.return(); 440 | if (throwFn) { 441 | throwFn.call(generator, e); 442 | } 443 | } 444 | }, 445 | return: { 446 | value: function(value) { 447 | stopGenerator.return(); 448 | if (returnFn) { 449 | returnFn.call(this, value); 450 | } 451 | } 452 | } 453 | })); 454 | 455 | return decoratedGenerator; 456 | }); 457 | }, 458 | /* 459 | flatten: function( 460 | errorBufferSize, 461 | errorBufferOverrunPolicy, // BUFFER_OVERRUN_POLICY.THROW | BUFFER_OVERRUN_POLICY.LATEST | BUFFER_OVERRUN_POLICY.OLDEST 462 | maxConcurrent, 463 | switchPolicy, // SWITCH.ON_COMPLETION | SWITCH.ON_ARRIVAL | SWITCH.ON_NOTIFICATION 464 | observableBufferSize, 465 | observableBufferOverrunPolicy, 466 | orderResults, 467 | itemsBufferSize, 468 | itemBufferOverrunPolicy) { } 469 | */ 470 | //TODO: Use symbol to avoid collision on index member added to iterator 471 | mergeAll: function(delayErrors) { 472 | var self = this; 473 | 474 | return new Observable(function observe(observer) { 475 | var indexSymbol = Symbol("index"), // key at which index of observer can be found. Index of each observer in observers map is stored at this symbol. This won't be necessary when we have Map. 476 | observers = Object.create(null), 477 | numObservers = 1, 478 | nextObserverIndex = 1, 479 | next = observer.next, 480 | errors = [], 481 | // finishes all inner and outer observation operations. 482 | onDone = function() { 483 | var key, 484 | innerObserver, 485 | returnFn; 486 | 487 | for(key in observers) { 488 | innerObserver = observers[key]; 489 | if (innerObserver) { 490 | returnFn = innerObserver.return; 491 | if (returnFn) { 492 | returnFn.call(innerObserver); 493 | } 494 | } 495 | } 496 | }, 497 | // The observer two which the merged output is sent. Finishes all inner and outer observation operations when terminated. 498 | decoratedObserver = decorate(observer, onDone), 499 | // The prototype used for the outer observer and all the inner observers 500 | // Both type of observer remove themself from the observers array, 501 | // then send a termination message to the decoratedObserver 502 | observerPrototype = { 503 | // In event of error, removes itself from observers 504 | throw: function(e) { 505 | delete observers[this[indexSymbol]]; 506 | numObservers--; 507 | 508 | errors.push(e); 509 | 510 | if (!delayErrors || numObservers === 0) { 511 | decoratedObserver.throw(errors.length > 1 ? {errors: errors} : errors[0]); 512 | } 513 | }, 514 | // In event of return, forwards on value if last observer, removes itself from observers 515 | return: function(v) { 516 | delete observers[this[indexSymbol]]; 517 | numObservers--; 518 | 519 | if (numObservers === 0) { 520 | decoratedObserver.return(v); 521 | } 522 | // NOTE THAT WE ONLY CAPTURE THE LAST RETURN VALUE, could add accumulater function to capture all return values. 523 | } 524 | }, 525 | observeInner = function(innerObservable) { 526 | // Wrap each inner observer and forward along any data received 527 | var innerObserver = { 528 | next: { 529 | value: function(value) { 530 | if (next) { 531 | return next.call(decoratedObserver, value); 532 | } 533 | } 534 | } 535 | }; 536 | innerObserver[indexSymbol] = { value: nextObserverIndex }; 537 | 538 | observers[nextObserverIndex] = 539 | innerObservable.observe( 540 | Object.create( 541 | observerPrototype, 542 | innerObserver)); 543 | 544 | numObservers++; 545 | nextObserverIndex++; 546 | }, 547 | outerObserver = 548 | Object.create( 549 | observerPrototype, 550 | { 551 | next: { 552 | value: observeInner 553 | } 554 | }); 555 | 556 | outerObserver[indexSymbol] = 0; 557 | observers[0] = self.observe(outerObserver); 558 | 559 | return decoratedObserver; 560 | }); 561 | }, 562 | concatAll: function(delayErrors) { 563 | var self = this; 564 | 565 | return new Observable(function observe(observer) { 566 | var indexSymbol = Symbol("index"), // key at which index of observer can be found. Index of each observer in observers map is stored at this symbol. This won't be necessary when we have Map. 567 | observers = {}, 568 | numObservers = 1, 569 | nextObserverIndex = 1, 570 | observables = [], 571 | next = observer.next, 572 | errors = [], 573 | onDone = function() { 574 | var key, 575 | innerObserver, 576 | returnFn; 577 | 578 | for(key in observers) { 579 | innerObserver = observers[key]; 580 | if (innerObserver) { 581 | returnFn = innerObserver.return; 582 | if (returnFn) { 583 | returnFn.call(innerObserver); 584 | } 585 | } 586 | } 587 | }, 588 | decoratedObserver = decorate(observer, onDone), 589 | observerPrototype = { 590 | throw: function(e) { 591 | delete observers[this[indexSymbol]]; 592 | numObservers--; 593 | observables.shift(); 594 | 595 | errors.push(e); 596 | 597 | if (delayErrors && (numObservers > 0 || observables.length > 0)) { 598 | if (observables.length > 0) { 599 | observeInner(); 600 | } 601 | } 602 | else { 603 | decoratedObserver.throw(errors.length > 1 ? {errors: errors} : errors[0]); 604 | } 605 | }, 606 | return: function(v) { 607 | delete observers[this[indexSymbol]]; 608 | numObservers--; 609 | observables.shift(); 610 | 611 | if (observables.length > 0) { 612 | observeInner(); 613 | } 614 | else if (observers[0] === undefined) { 615 | decoratedObserver.return(v); 616 | } 617 | } 618 | }, 619 | observeInner = function() { 620 | var innerObservable = observables[0], 621 | innerObserver = 622 | Object.create( 623 | observerPrototype, 624 | { 625 | next: { 626 | value: function(value) { 627 | if (next) { 628 | return next.call(decoratedObserver, value); 629 | } 630 | } 631 | } 632 | }); 633 | innerObserver[indexSymbol] = nextObserverIndex; 634 | 635 | if (innerObservable) { 636 | observers[nextObserverIndex] = innerObservable.observe(innerObserver); 637 | 638 | numObservers++; 639 | nextObserverIndex++; 640 | } 641 | }, 642 | outerObserver = 643 | Object.create( 644 | observerPrototype, 645 | { 646 | next: { 647 | value: function(innerObservable) { 648 | observables.push(innerObservable); 649 | observeInner(); 650 | } 651 | } 652 | }); 653 | outerObserver[indexSymbol] = 0; 654 | 655 | observers[0] = self.observe(outerObserver); 656 | 657 | return decoratedObserver; 658 | }); 659 | }, 660 | switchLatest: function(delayErrors) { 661 | var self = this; 662 | 663 | return new Observable(function observe(observer) { 664 | var indexSymbol = Symbol("index"), // key at which index of observer can be found. Index of each observer in observers map is stored at this symbol. This won't be necessary when we have Map. 665 | innerObservable, 666 | observers = {}, 667 | numObservers = 0, 668 | errors = [], 669 | onDone = function() { 670 | var key, 671 | innerObserver, 672 | returnFn; 673 | 674 | if (innerObserver = observers[numObservers - 1]) { 675 | innerObserver.return(); 676 | } 677 | 678 | if (outerObserver) { 679 | outerObserver.return(); 680 | } 681 | }, 682 | decoratedObserver = decorate(observer, onDone), 683 | innerObserverPrototype = { 684 | throw: function(e) { 685 | delete observers[this[indexSymbol]]; 686 | 687 | errors.push(e); 688 | 689 | if (!delayErrors || !outerObserver) { 690 | return decoratedObserver.throw(errors.length > 1 ? {errors: errors} : errors[0]); 691 | } 692 | }, 693 | return: function(v) { 694 | delete observers[this[indexSymbol]]; 695 | 696 | if (!outerObserver) { 697 | return decoratedObserver.return(v); 698 | } 699 | } 700 | }, 701 | outerObserver = 702 | Object.create( 703 | { 704 | throw: function(e) { 705 | var innerObserver = observers[numObservers - 1]; 706 | outerObserver = undefined; 707 | 708 | errors.push(e); 709 | 710 | if (!delayErrors || !innerObserver) { 711 | return decoratedObserver.throw(errors.length > 1 ? {errors: errors} : errors[0]); 712 | } 713 | }, 714 | return: function(v) { 715 | var innerObserver = observers[numObservers - 1]; 716 | outerObserver = undefined; 717 | 718 | if (!innerObserver) { 719 | return decoratedObserver.return(v); 720 | } 721 | } 722 | }, 723 | { 724 | next: { 725 | value: function(innerObservable) { 726 | var innerObserver = observers[numObservers - 1]; 727 | 728 | if (innerObserver) { 729 | innerObserver.return(); 730 | } 731 | 732 | innerObserver = 733 | Object.create( 734 | innerObserverPrototype, 735 | { 736 | next: { 737 | value: function(value) { 738 | return decoratedObserver.next(value); 739 | } 740 | } 741 | }); 742 | 743 | innerObserver[indexSymbol] = numObservers; 744 | observers[numObservers] = innerObservable.observe(innerObserver); 745 | numObservers++; 746 | } 747 | } 748 | }); 749 | 750 | outerObserver = self.observe(outerObserver); 751 | 752 | return decoratedObserver; 753 | }); 754 | }, 755 | /*exclusive: function(delayErrors) { 756 | var self = this; 757 | 758 | return new Observable(function observe(observer) { 759 | var indexSymbol = Symbol("index"), // key at which index of observer can be found. Index of each observer in observers map is stored at this symbol. This won't be necessary when we have Map. 760 | observers = {}, 761 | numObservers = 1, 762 | next = observer.next, 763 | errors = [], 764 | onDone = function() { 765 | var key, 766 | innerObserver, 767 | returnFn; 768 | 769 | for(key in observers) { 770 | innerObserver = observers[key]; 771 | if (innerObserver) { 772 | returnFn = innerObserver.return; 773 | if (returnFn) { 774 | returnFn.call(innerObserver); 775 | } 776 | } 777 | } 778 | }, 779 | decoratedObserver = decorate(observer, onDone), 780 | observerPrototype = { 781 | throw: function(e) { 782 | observers[this[indexSymbol]] = undefined; 783 | numObservers--; 784 | 785 | errors.push(e); 786 | 787 | if (delayErrors && numObservers > 0) { 788 | if (observable[1]) { 789 | observeInner(); 790 | } 791 | } 792 | else { 793 | decoratedObserver.throw(errors.length > 1 ? {errors: errors} : errors[0]); 794 | } 795 | }, 796 | return: function(v) { 797 | observers[this[indexSymbol]] = undefined; 798 | numObservers--; 799 | 800 | if (observable) { 801 | observeInner(); 802 | } 803 | else if (observers[0] === undefined) { 804 | if (returnFn) { 805 | result = returnFn.call(decoratedObserver, v); 806 | } 807 | } 808 | } 809 | }, 810 | observeInner = function() { 811 | var innerObservable = observable, 812 | innerObserver; 813 | observable = undefined; 814 | 815 | if (innerObservable) { 816 | innerObserver = 817 | Object.create( 818 | observerPrototype, 819 | { 820 | next: { 821 | value: function(value) { 822 | if (next) { 823 | return next.call(decoratedObserver, value); 824 | } 825 | } 826 | } 827 | }); 828 | innerObserver[indexSymbol] = 1; 829 | 830 | observers[1] = innerObservable.observe(innerObserver); 831 | 832 | numObservers++; 833 | } 834 | }, 835 | outerObserver = 836 | Object.create( 837 | observerPrototype, 838 | { 839 | next: { 840 | value: function(innerObservable) { 841 | observable = innerObservable; 842 | if (observers[1]) { 843 | observers[1].return(); 844 | } 845 | else { 846 | observeInner(); 847 | } 848 | } 849 | } 850 | }); 851 | outerObserver[indexSymbol] = 0; 852 | 853 | observers[0] = self.observe(outerObserver); 854 | 855 | return decoratedObserver; 856 | }); 857 | },*/ 858 | merge: function() { 859 | return Observable.from( 860 | [this].concat( 861 | Array.prototype.slice. 862 | call(arguments). 863 | map(function(i) { 864 | return i instanceof Observable ? i : Observable.of(i); 865 | }))). 866 | mergeAll(); 867 | }, 868 | concat: function() { 869 | return Observable.from( 870 | [this].concat( 871 | Array.prototype.slice. 872 | call(arguments). 873 | map(function(i) { 874 | return i instanceof Observable ? i : Observable.of(i); 875 | }))). 876 | concatAll(); 877 | }, 878 | mergeMap: function(projection) { 879 | return this.map(projection).mergeAll(); 880 | }, 881 | concatMap: function(projection) { 882 | return this.map(projection).concatAll(); 883 | }, 884 | switchMap: function(projection) { 885 | return this.map(projection).switchLatest(); 886 | }, 887 | exclusiveMap: function(projection) { 888 | return this.map(projection).exclusive(); 889 | } 890 | }; 891 | 892 | 893 | Observable.fromWebSocket = function(ws) { 894 | // An Observable is created by passing the defn of its observer method to its constructor 895 | return new Observable(function observer(generator) { 896 | function message(m) { 897 | decoratedGenerator.next(m); 898 | } 899 | 900 | function close() { 901 | done = true; 902 | decoratedGenerator.return(); 903 | } 904 | 905 | function error(e) { 906 | done = true; 907 | decoratedGenerator.throw(e); 908 | } 909 | 910 | var done = false, 911 | decoratedGenerator = 912 | decorate( 913 | generator, 914 | function() { 915 | if (!done) { 916 | done = true; 917 | ws.close(); 918 | ws.removeEventListener('message', message); 919 | ws.removeEventListener('close', close); 920 | ws.removeEventListener('error', error); 921 | 922 | } 923 | }); 924 | 925 | ws.addEventListener('message', message); 926 | ws.addEventListener('close', close); 927 | ws.addEventListener('error', error); 928 | 929 | return decoratedGenerator; 930 | }); 931 | }; 932 | 933 | 934 | Observable.prototype.unwrapPromises = function() { 935 | return this.lift( 936 | function(generator) { 937 | return Object.create( 938 | generator, 939 | { 940 | next: { 941 | value: function(value) { 942 | var next; 943 | if (generator.next) { 944 | next = generator.next.bind(generator) 945 | } 946 | var error; 947 | if (generator.throw) { 948 | error = generator.throw.bind(generator); 949 | } 950 | value.then(next, error); 951 | } 952 | } 953 | }) 954 | }); 955 | }; 956 | -------------------------------------------------------------------------------- /public/polyfills.js: -------------------------------------------------------------------------------- 1 | if (!Array.prototype.find) { 2 | Array.prototype.find = function(predicate) { 3 | if (this == null) { 4 | throw new TypeError('Array.prototype.find called on null or undefined'); 5 | } 6 | if (typeof predicate !== 'function') { 7 | throw new TypeError('predicate must be a function'); 8 | } 9 | var list = Object(this); 10 | var length = list.length >>> 0; 11 | var thisArg = arguments[1]; 12 | var value; 13 | 14 | for (var i = 0; i < length; i++) { 15 | value = list[i]; 16 | if (predicate.call(thisArg, value, i, list)) { 17 | return value; 18 | } 19 | } 20 | return undefined; 21 | }; 22 | } 23 | 24 | // Production steps of ECMA-262, Edition 6, 22.1.2.1 25 | // Reference: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-array.from 26 | if (!Array.from) { 27 | Array.from = (function () { 28 | var toStr = Object.prototype.toString; 29 | var isCallable = function (fn) { 30 | return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; 31 | }; 32 | var toInteger = function (value) { 33 | var number = Number(value); 34 | if (isNaN(number)) { return 0; } 35 | if (number === 0 || !isFinite(number)) { return number; } 36 | return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); 37 | }; 38 | var maxSafeInteger = Math.pow(2, 53) - 1; 39 | var toLength = function (value) { 40 | var len = toInteger(value); 41 | return Math.min(Math.max(len, 0), maxSafeInteger); 42 | }; 43 | 44 | // The length property of the from method is 1. 45 | return function from(arrayLike/*, mapFn, thisArg */) { 46 | // 1. Let C be the this value. 47 | var C = this; 48 | 49 | // 2. Let items be ToObject(arrayLike). 50 | var items = Object(arrayLike); 51 | 52 | // 3. ReturnIfAbrupt(items). 53 | if (arrayLike == null) { 54 | throw new TypeError("Array.from requires an array-like object - not null or undefined"); 55 | } 56 | 57 | // 4. If mapfn is undefined, then let mapping be false. 58 | var mapFn = arguments.length > 1 ? arguments[1] : void undefined; 59 | var T; 60 | if (typeof mapFn !== 'undefined') { 61 | // 5. else 62 | // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. 63 | if (!isCallable(mapFn)) { 64 | throw new TypeError('Array.from: when provided, the second argument must be a function'); 65 | } 66 | 67 | // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. 68 | if (arguments.length > 2) { 69 | T = arguments[2]; 70 | } 71 | } 72 | 73 | // 10. Let lenValue be Get(items, "length"). 74 | // 11. Let len be ToLength(lenValue). 75 | var len = toLength(items.length); 76 | 77 | // 13. If IsConstructor(C) is true, then 78 | // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len. 79 | // 14. a. Else, Let A be ArrayCreate(len). 80 | var A = isCallable(C) ? Object(new C(len)) : new Array(len); 81 | 82 | // 16. Let k be 0. 83 | var k = 0; 84 | // 17. Repeat, while k < len… (also steps a - h) 85 | var kValue; 86 | while (k < len) { 87 | kValue = items[k]; 88 | if (mapFn) { 89 | A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); 90 | } else { 91 | A[k] = kValue; 92 | } 93 | k += 1; 94 | } 95 | // 18. Let putStatus be Put(A, "length", len, true). 96 | A.length = len; 97 | // 20. Return A. 98 | return A; 99 | }; 100 | }()); 101 | } 102 | 103 | if (!Uint8Array.prototype.map) { 104 | Uint8Array.prototype.map = Array.prototype.map; 105 | } 106 | -------------------------------------------------------------------------------- /public/qrcode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * - Using the 'QRCode for Javascript library' 4 | * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. 5 | * - this library has no dependencies. 6 | * 7 | * @author davidshimjs 8 | * @see http://www.d-project.com/ 9 | * @see http://jeromeetienne.github.com/jquery-qrcode/ 10 | */ 11 | var QRCode; 12 | 13 | (function () { 14 | //--------------------------------------------------------------------- 15 | // QRCode for JavaScript 16 | // 17 | // Copyright (c) 2009 Kazuhiko Arase 18 | // 19 | // URL: http://www.d-project.com/ 20 | // 21 | // Licensed under the MIT license: 22 | // http://www.opensource.org/licenses/mit-license.php 23 | // 24 | // The word "QR Code" is registered trademark of 25 | // DENSO WAVE INCORPORATED 26 | // http://www.denso-wave.com/qrcode/faqpatent-e.html 27 | // 28 | //--------------------------------------------------------------------- 29 | function QR8bitByte(data) { 30 | this.mode = QRMode.MODE_8BIT_BYTE; 31 | this.data = data; 32 | this.parsedData = []; 33 | 34 | // Added to support UTF-8 Characters 35 | for (var i = 0, l = this.data.length; i < l; i++) { 36 | var byteArray = []; 37 | var code = this.data.charCodeAt(i); 38 | 39 | if (code > 0x10000) { 40 | byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); 41 | byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); 42 | byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); 43 | byteArray[3] = 0x80 | (code & 0x3F); 44 | } else if (code > 0x800) { 45 | byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); 46 | byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); 47 | byteArray[2] = 0x80 | (code & 0x3F); 48 | } else if (code > 0x80) { 49 | byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); 50 | byteArray[1] = 0x80 | (code & 0x3F); 51 | } else { 52 | byteArray[0] = code; 53 | } 54 | 55 | this.parsedData.push(byteArray); 56 | } 57 | 58 | this.parsedData = Array.prototype.concat.apply([], this.parsedData); 59 | 60 | if (this.parsedData.length != this.data.length) { 61 | this.parsedData.unshift(191); 62 | this.parsedData.unshift(187); 63 | this.parsedData.unshift(239); 64 | } 65 | } 66 | 67 | QR8bitByte.prototype = { 68 | getLength: function (buffer) { 69 | return this.parsedData.length; 70 | }, 71 | write: function (buffer) { 72 | for (var i = 0, l = this.parsedData.length; i < l; i++) { 73 | buffer.put(this.parsedData[i], 8); 74 | } 75 | } 76 | }; 77 | 78 | function QRCodeModel(typeNumber, errorCorrectLevel) { 79 | this.typeNumber = typeNumber; 80 | this.errorCorrectLevel = errorCorrectLevel; 81 | this.modules = null; 82 | this.moduleCount = 0; 83 | this.dataCache = null; 84 | this.dataList = []; 85 | } 86 | 87 | QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} 88 | return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row=7){this.setupTypeNumber(test);} 90 | if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} 91 | this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} 92 | return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} 98 | for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} 99 | for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} 100 | this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex>>bitIndex)&1)==1);} 101 | var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} 102 | this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} 103 | row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;itotalDataCount*8){throw new Error("code length overflow. (" 106 | +buffer.getLengthInBits() 107 | +">" 108 | +totalDataCount*8 109 | +")");} 110 | if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} 111 | while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} 112 | while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} 113 | buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} 114 | buffer.put(QRCodeModel.PAD1,8);} 115 | return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r=0)?modPoly.get(modIndex):0;}} 117 | var totalCodeCount=0;for(var i=0;i=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} 121 | return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} 122 | return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} 123 | return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i5){lostPoint+=(3+sameCount-5);}}} 129 | for(var row=0;row=256){n-=255;} 136 | return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} 151 | if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} 152 | this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; 153 | 154 | function _isSupportCanvas() { 155 | return typeof CanvasRenderingContext2D != "undefined"; 156 | } 157 | 158 | // android 2.x doesn't support Data-URI spec 159 | function _getAndroid() { 160 | var android = false; 161 | var sAgent = navigator.userAgent; 162 | 163 | if (/android/i.test(sAgent)) { // android 164 | android = true; 165 | var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); 166 | 167 | if (aMat && aMat[1]) { 168 | android = parseFloat(aMat[1]); 169 | } 170 | } 171 | 172 | return android; 173 | } 174 | 175 | var svgDrawer = (function() { 176 | 177 | var Drawing = function (el, htOption) { 178 | this._el = el; 179 | this._htOption = htOption; 180 | }; 181 | 182 | Drawing.prototype.draw = function (oQRCode) { 183 | var _htOption = this._htOption; 184 | var _el = this._el; 185 | var nCount = oQRCode.getModuleCount(); 186 | var nWidth = Math.floor(_htOption.width / nCount); 187 | var nHeight = Math.floor(_htOption.height / nCount); 188 | 189 | this.clear(); 190 | 191 | function makeSVG(tag, attrs) { 192 | var el = document.createElementNS('http://www.w3.org/2000/svg', tag); 193 | for (var k in attrs) 194 | if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); 195 | return el; 196 | } 197 | 198 | var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); 199 | svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); 200 | _el.appendChild(svg); 201 | 202 | svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"})); 203 | svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); 204 | 205 | for (var row = 0; row < nCount; row++) { 206 | for (var col = 0; col < nCount; col++) { 207 | if (oQRCode.isDark(row, col)) { 208 | var child = makeSVG("use", {"x": String(row), "y": String(col)}); 209 | child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") 210 | svg.appendChild(child); 211 | } 212 | } 213 | } 214 | }; 215 | Drawing.prototype.clear = function () { 216 | while (this._el.hasChildNodes()) 217 | this._el.removeChild(this._el.lastChild); 218 | }; 219 | return Drawing; 220 | })(); 221 | 222 | var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; 223 | 224 | // Drawing in DOM by using Table tag 225 | var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { 226 | var Drawing = function (el, htOption) { 227 | this._el = el; 228 | this._htOption = htOption; 229 | }; 230 | 231 | /** 232 | * Draw the QRCode 233 | * 234 | * @param {QRCode} oQRCode 235 | */ 236 | Drawing.prototype.draw = function (oQRCode) { 237 | var _htOption = this._htOption; 238 | var _el = this._el; 239 | var nCount = oQRCode.getModuleCount(); 240 | var nWidth = Math.floor(_htOption.width / nCount); 241 | var nHeight = Math.floor(_htOption.height / nCount); 242 | var aHTML = ['']; 243 | 244 | for (var row = 0; row < nCount; row++) { 245 | aHTML.push(''); 246 | 247 | for (var col = 0; col < nCount; col++) { 248 | aHTML.push(''); 249 | } 250 | 251 | aHTML.push(''); 252 | } 253 | 254 | aHTML.push('
'); 255 | _el.innerHTML = aHTML.join(''); 256 | 257 | // Fix the margin values as real size. 258 | var elTable = _el.childNodes[0]; 259 | var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; 260 | var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; 261 | 262 | if (nLeftMarginTable > 0 && nTopMarginTable > 0) { 263 | elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; 264 | } 265 | }; 266 | 267 | /** 268 | * Clear the QRCode 269 | */ 270 | Drawing.prototype.clear = function () { 271 | this._el.innerHTML = ''; 272 | }; 273 | 274 | return Drawing; 275 | })() : (function () { // Drawing in Canvas 276 | function _onMakeImage() { 277 | this._elImage.src = this._elCanvas.toDataURL("image/png"); 278 | this._elImage.style.display = "block"; 279 | this._elCanvas.style.display = "none"; 280 | } 281 | 282 | // Android 2.1 bug workaround 283 | // http://code.google.com/p/android/issues/detail?id=5141 284 | if (this._android && this._android <= 2.1) { 285 | var factor = 1 / window.devicePixelRatio; 286 | var drawImage = CanvasRenderingContext2D.prototype.drawImage; 287 | CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { 288 | if (("nodeName" in image) && /img/i.test(image.nodeName)) { 289 | for (var i = arguments.length - 1; i >= 1; i--) { 290 | arguments[i] = arguments[i] * factor; 291 | } 292 | } else if (typeof dw == "undefined") { 293 | arguments[1] *= factor; 294 | arguments[2] *= factor; 295 | arguments[3] *= factor; 296 | arguments[4] *= factor; 297 | } 298 | 299 | drawImage.apply(this, arguments); 300 | }; 301 | } 302 | 303 | /** 304 | * Check whether the user's browser supports Data URI or not 305 | * 306 | * @private 307 | * @param {Function} fSuccess Occurs if it supports Data URI 308 | * @param {Function} fFail Occurs if it doesn't support Data URI 309 | */ 310 | function _safeSetDataURI(fSuccess, fFail) { 311 | var self = this; 312 | self._fFail = fFail; 313 | self._fSuccess = fSuccess; 314 | 315 | // Check it just once 316 | if (self._bSupportDataURI === null) { 317 | var el = document.createElement("img"); 318 | var fOnError = function() { 319 | self._bSupportDataURI = false; 320 | 321 | if (self._fFail) { 322 | self._fFail.call(self); 323 | } 324 | }; 325 | var fOnSuccess = function() { 326 | self._bSupportDataURI = true; 327 | 328 | if (self._fSuccess) { 329 | self._fSuccess.call(self); 330 | } 331 | }; 332 | 333 | el.onabort = fOnError; 334 | el.onerror = fOnError; 335 | el.onload = fOnSuccess; 336 | el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. 337 | return; 338 | } else if (self._bSupportDataURI === true && self._fSuccess) { 339 | self._fSuccess.call(self); 340 | } else if (self._bSupportDataURI === false && self._fFail) { 341 | self._fFail.call(self); 342 | } 343 | }; 344 | 345 | /** 346 | * Drawing QRCode by using canvas 347 | * 348 | * @constructor 349 | * @param {HTMLElement} el 350 | * @param {Object} htOption QRCode Options 351 | */ 352 | var Drawing = function (el, htOption) { 353 | this._bIsPainted = false; 354 | this._android = _getAndroid(); 355 | 356 | this._htOption = htOption; 357 | this._elCanvas = document.createElement("canvas"); 358 | this._elCanvas.width = htOption.width; 359 | this._elCanvas.height = htOption.height; 360 | el.appendChild(this._elCanvas); 361 | this._el = el; 362 | this._oContext = this._elCanvas.getContext("2d"); 363 | this._bIsPainted = false; 364 | this._elImage = document.createElement("img"); 365 | this._elImage.alt = "Scan me!"; 366 | this._elImage.style.display = "none"; 367 | this._el.appendChild(this._elImage); 368 | this._bSupportDataURI = null; 369 | }; 370 | 371 | /** 372 | * Draw the QRCode 373 | * 374 | * @param {QRCode} oQRCode 375 | */ 376 | Drawing.prototype.draw = function (oQRCode) { 377 | var _elImage = this._elImage; 378 | var _oContext = this._oContext; 379 | var _htOption = this._htOption; 380 | 381 | var nCount = oQRCode.getModuleCount(); 382 | var nWidth = _htOption.width / nCount; 383 | var nHeight = _htOption.height / nCount; 384 | var nRoundedWidth = Math.round(nWidth); 385 | var nRoundedHeight = Math.round(nHeight); 386 | 387 | _elImage.style.display = "none"; 388 | this.clear(); 389 | 390 | for (var row = 0; row < nCount; row++) { 391 | for (var col = 0; col < nCount; col++) { 392 | var bIsDark = oQRCode.isDark(row, col); 393 | var nLeft = col * nWidth; 394 | var nTop = row * nHeight; 395 | _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; 396 | _oContext.lineWidth = 1; 397 | _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; 398 | _oContext.fillRect(nLeft, nTop, nWidth, nHeight); 399 | 400 | // 안티 앨리어싱 방지 처리 401 | _oContext.strokeRect( 402 | Math.floor(nLeft) + 0.5, 403 | Math.floor(nTop) + 0.5, 404 | nRoundedWidth, 405 | nRoundedHeight 406 | ); 407 | 408 | _oContext.strokeRect( 409 | Math.ceil(nLeft) - 0.5, 410 | Math.ceil(nTop) - 0.5, 411 | nRoundedWidth, 412 | nRoundedHeight 413 | ); 414 | } 415 | } 416 | 417 | this._bIsPainted = true; 418 | }; 419 | 420 | /** 421 | * Make the image from Canvas if the browser supports Data URI. 422 | */ 423 | Drawing.prototype.makeImage = function () { 424 | if (this._bIsPainted) { 425 | _safeSetDataURI.call(this, _onMakeImage); 426 | } 427 | }; 428 | 429 | /** 430 | * Return whether the QRCode is painted or not 431 | * 432 | * @return {Boolean} 433 | */ 434 | Drawing.prototype.isPainted = function () { 435 | return this._bIsPainted; 436 | }; 437 | 438 | /** 439 | * Clear the QRCode 440 | */ 441 | Drawing.prototype.clear = function () { 442 | this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); 443 | this._bIsPainted = false; 444 | }; 445 | 446 | /** 447 | * @private 448 | * @param {Number} nNumber 449 | */ 450 | Drawing.prototype.round = function (nNumber) { 451 | if (!nNumber) { 452 | return nNumber; 453 | } 454 | 455 | return Math.floor(nNumber * 1000) / 1000; 456 | }; 457 | 458 | return Drawing; 459 | })(); 460 | 461 | /** 462 | * Get the type by string length 463 | * 464 | * @private 465 | * @param {String} sText 466 | * @param {Number} nCorrectLevel 467 | * @return {Number} type 468 | */ 469 | function _getTypeNumber(sText, nCorrectLevel) { 470 | var nType = 1; 471 | var length = _getUTF8Length(sText); 472 | 473 | for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { 474 | var nLimit = 0; 475 | 476 | switch (nCorrectLevel) { 477 | case QRErrorCorrectLevel.L : 478 | nLimit = QRCodeLimitLength[i][0]; 479 | break; 480 | case QRErrorCorrectLevel.M : 481 | nLimit = QRCodeLimitLength[i][1]; 482 | break; 483 | case QRErrorCorrectLevel.Q : 484 | nLimit = QRCodeLimitLength[i][2]; 485 | break; 486 | case QRErrorCorrectLevel.H : 487 | nLimit = QRCodeLimitLength[i][3]; 488 | break; 489 | } 490 | 491 | if (length <= nLimit) { 492 | break; 493 | } else { 494 | nType++; 495 | } 496 | } 497 | 498 | if (nType > QRCodeLimitLength.length) { 499 | throw new Error("Too long data"); 500 | } 501 | 502 | return nType; 503 | } 504 | 505 | function _getUTF8Length(sText) { 506 | var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); 507 | return replacedText.length + (replacedText.length != sText ? 3 : 0); 508 | } 509 | 510 | /** 511 | * @class QRCode 512 | * @constructor 513 | * @example 514 | * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); 515 | * 516 | * @example 517 | * var oQRCode = new QRCode("test", { 518 | * text : "http://naver.com", 519 | * width : 128, 520 | * height : 128 521 | * }); 522 | * 523 | * oQRCode.clear(); // Clear the QRCode. 524 | * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. 525 | * 526 | * @param {HTMLElement|String} el target element or 'id' attribute of element. 527 | * @param {Object|String} vOption 528 | * @param {String} vOption.text QRCode link data 529 | * @param {Number} [vOption.width=256] 530 | * @param {Number} [vOption.height=256] 531 | * @param {String} [vOption.colorDark="#000000"] 532 | * @param {String} [vOption.colorLight="#ffffff"] 533 | * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] 534 | */ 535 | QRCode = function (el, vOption) { 536 | this._htOption = { 537 | width : 256, 538 | height : 256, 539 | typeNumber : 4, 540 | colorDark : "#000000", 541 | colorLight : "#ffffff", 542 | correctLevel : QRErrorCorrectLevel.H 543 | }; 544 | 545 | if (typeof vOption === 'string') { 546 | vOption = { 547 | text : vOption 548 | }; 549 | } 550 | 551 | // Overwrites options 552 | if (vOption) { 553 | for (var i in vOption) { 554 | this._htOption[i] = vOption[i]; 555 | } 556 | } 557 | 558 | if (typeof el == "string") { 559 | el = document.getElementById(el); 560 | } 561 | 562 | if (this._htOption.useSVG) { 563 | Drawing = svgDrawer; 564 | } 565 | 566 | this._android = _getAndroid(); 567 | this._el = el; 568 | this._oQRCode = null; 569 | this._oDrawing = new Drawing(this._el, this._htOption); 570 | 571 | if (this._htOption.text) { 572 | this.makeCode(this._htOption.text); 573 | } 574 | }; 575 | 576 | /** 577 | * Make the QRCode 578 | * 579 | * @param {String} sText link data 580 | */ 581 | QRCode.prototype.makeCode = function (sText) { 582 | this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); 583 | this._oQRCode.addData(sText); 584 | this._oQRCode.make(); 585 | this._el.title = sText; 586 | this._oDrawing.draw(this._oQRCode); 587 | this.makeImage(); 588 | }; 589 | 590 | /** 591 | * Make the Image from Canvas element 592 | * - It occurs automatically 593 | * - Android below 3 doesn't support Data-URI spec. 594 | * 595 | * @private 596 | */ 597 | QRCode.prototype.makeImage = function () { 598 | if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { 599 | this._oDrawing.makeImage(); 600 | } 601 | }; 602 | 603 | /** 604 | * Clear the QRCode 605 | */ 606 | QRCode.prototype.clear = function () { 607 | this._oDrawing.clear(); 608 | }; 609 | 610 | /** 611 | * @name QRCode.CorrectLevel 612 | */ 613 | QRCode.CorrectLevel = QRErrorCorrectLevel; 614 | })(); 615 | -------------------------------------------------------------------------------- /public/rtcpolyfills.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Curiosity driven 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection; 18 | var RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription; 19 | var RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate; 20 | 21 | navigator.mediaDevices = navigator.mediaDevices || {}; 22 | navigator.mediaDevices.getUserMedia = navigator.mediaDevices.getUserMedia || function(constraints) { 23 | var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; 24 | 25 | return new Promise(getUserMedia.bind(navigator, constraints)); 26 | } 27 | 28 | if (RTCPeerConnection.prototype.createAnswer.length > 0) { 29 | window.RTCPeerConnection = (function(RTCPeerConnection) { 30 | function RTCPromisePeerConnection(constraints) { 31 | this.connection = new RTCPeerConnection(constraints); 32 | } 33 | 34 | RTCPromisePeerConnection.prototype = { 35 | addStream: function() { 36 | this.connection.addStream.apply(this.connection, arguments); 37 | }, 38 | createOffer: function() { 39 | return new Promise(this.connection.createOffer.bind(this.connection)); 40 | }, 41 | createAnswer: function() { 42 | return new Promise(this.connection.createAnswer.bind(this.connection)); 43 | }, 44 | setLocalDescription: function(sessionDescription) { 45 | return new Promise(this.connection.setLocalDescription.bind(this.connection, sessionDescription)); 46 | }, 47 | setRemoteDescription: function(sessionDescription) { 48 | return new Promise(this.connection.setRemoteDescription.bind(this.connection, sessionDescription)); 49 | }, 50 | addIceCandidate: function(candidate) { 51 | return new Promise(this.connection.addIceCandidate.bind(this.connection, candidate)); 52 | }, 53 | addEventListener: function(event, listener) { 54 | this.connection.addEventListener(event, listener); 55 | }, 56 | removeEventListener: function(event, listener) { 57 | this.connection.removeEventListener(event, listener); 58 | } 59 | }; 60 | 61 | return RTCPromisePeerConnection; 62 | 63 | }(window.RTCPeerConnection)); 64 | } 65 | -------------------------------------------------------------------------------- /public/scripts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Curiosity driven 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | function WebSocketSignalingChannel(address) { 18 | this.socket = new WebSocket(address); 19 | var socketObservable = Observable.fromWebSocket(this.socket); 20 | this.messages = socketObservable.map(function(event) { 21 | return JSON.parse(event.data); 22 | }); 23 | } 24 | 25 | WebSocketSignalingChannel.prototype.next = function(object) { 26 | this.socket.send(JSON.stringify(object)); 27 | }; 28 | 29 | function getRoom() { 30 | var room = location.search.substring(1); 31 | if (room.length < 20) { 32 | room = crypto.getRandomValues(new Uint8Array(10)).map(function(b) { 33 | return b.toString(16); 34 | }).map(function pad(b) { 35 | return ('00' + b).slice(-2); 36 | }).join(''); 37 | history.replaceState(null, '', '?' + room + '#conference'); 38 | } 39 | return room; 40 | } 41 | 42 | function getSignalingChannel(room) { 43 | var url = location.protocol.replace('http', 'ws') + '//' + location.host + '/rooms/' + room; 44 | return new WebSocketSignalingChannel(url); 45 | } 46 | 47 | RTCPeer.prototype.handleMessage = function(body) { 48 | var peer = this.peer, type = body.type; 49 | 50 | if (type === 'join') { 51 | // peer 1 52 | return peer.createOffer().then(this.setLocal); 53 | } else if (type === 'offer') { 54 | // peer 2 55 | peer.setRemoteDescription(new RTCSessionDescription(body)); 56 | return peer.createAnswer().then(this.setLocal); 57 | } else if (type === 'answer') { 58 | // peer 1 59 | peer.setRemoteDescription(new RTCSessionDescription(body)); 60 | } else if (type === 'candidate') { 61 | // both peers 62 | peer.addIceCandidate(new RTCIceCandidate(body)); 63 | } 64 | 65 | return Promise.resolve({ target: self }); 66 | }; 67 | 68 | function getReplies(messages, peers) { 69 | return messages.map(function(message) { 70 | return peers.get(message.from).handleMessage(message.body); 71 | }).unwrapPromises().filter(function(reply) { 72 | return reply.response; 73 | }).map(function(reply) { 74 | return { 75 | to: reply.target.id, 76 | body: reply.response 77 | }; 78 | }); 79 | } 80 | 81 | function RTCPeer(id, peer) { 82 | this.id = id; 83 | this.peer = peer; 84 | 85 | var iceEvents = Observable.fromEvent(peer, 'icecandidate'); 86 | this.iceCandidates = iceEvents.filter(function(event) { 87 | return event.candidate; 88 | }).map(function(event) { 89 | return { 90 | target: this, 91 | candidate: event.candidate 92 | }; 93 | }, this); 94 | 95 | var addStreamEvents = Observable.fromEvent(peer, 'addstream'); 96 | this.streams = addStreamEvents.map(function(event) { 97 | return { 98 | target: this, 99 | stream: event.stream 100 | }; 101 | }, this); 102 | 103 | this.setLocal = this.setLocal.bind(this); 104 | } 105 | 106 | RTCPeer.prototype.setLocal = function(description) { 107 | return this.peer.setLocalDescription(description).then(function() { 108 | return { target: this, response: description }; 109 | }.bind(this)); 110 | }; 111 | 112 | function getCandidates(objects) { 113 | return objects.mergeMap(function(peer) { 114 | return peer.iceCandidates; 115 | }).map(function(event) { 116 | var candidate = JSON.parse(JSON.stringify(event.candidate)); 117 | candidate.type = 'candidate'; 118 | return { 119 | to: event.target.id, 120 | body: candidate 121 | }; 122 | }); 123 | } 124 | 125 | function getRemoteStreams(objects) { 126 | return objects.mergeMap(function(peer) { 127 | return peer.streams; 128 | }).map(function(event) { 129 | return event.stream; 130 | }); 131 | } 132 | 133 | function LazyMap(create) { 134 | var map = new Map, generators = []; 135 | this.get = function(key) { 136 | if (map.has(key)) { 137 | return map.get(key); 138 | } 139 | var value = create(key); 140 | map.set(key, value); 141 | generators.forEach(function(generator) { 142 | generator.next(value); 143 | }); 144 | return value; 145 | }; 146 | this.objects = new Observable(function observe(generator) { 147 | generators.push(generator) 148 | return generator; 149 | }); 150 | } 151 | 152 | function createMap(stream) { 153 | var configuration = { 154 | iceServers: [{ 155 | url: 'stun:stun.l.google.com:19302' 156 | }, { 157 | url: 'stun:stun.services.mozilla.com' 158 | }, { 159 | url: 'turn:turn.bistri.com:80', 160 | credential: 'homeo', 161 | username: 'homeo' 162 | }] 163 | }; 164 | return new LazyMap(function(id) { 165 | var peer = new RTCPeerConnection(configuration); 166 | peer.addStream(stream); 167 | return new RTCPeer(id, peer); 168 | }); 169 | } 170 | 171 | function UI(container) { 172 | var bigRemoteView = container.querySelector('.remote'); 173 | var selfView = container.querySelector('.self'); 174 | var statusLabel = container.querySelector('.status'); 175 | var substatusLabel = container.querySelector('.substatus'); 176 | var participants = container.querySelector('.participants'); 177 | 178 | function setParticipantsView() { 179 | var videos = participants.querySelectorAll('video:not(.self)'); 180 | Array.from(videos).forEach(function(video) { 181 | video.hidden = video.src === bigRemoteView.src; 182 | }); 183 | } 184 | 185 | function setBigSource() { 186 | bigRemoteView.src = this.src; 187 | setParticipantsView(); 188 | } 189 | 190 | function addRemoteStream(stream) { 191 | var remoteView = document.createElement('video'); 192 | participants.insertBefore(remoteView, participants.firstChild); 193 | 194 | remoteView.addEventListener('click', setBigSource); 195 | remoteView.autoplay = true; 196 | remoteView.src = URL.createObjectURL(stream); 197 | 198 | if (!bigRemoteView.src) { 199 | setBigSource.call(remoteView); 200 | } 201 | } 202 | 203 | var qrCode = new QRCode(substatusLabel.querySelector('.qr-link'), { 204 | width: 128, 205 | height: 128 206 | }); 207 | 208 | return { 209 | updateStatus: function(message) { 210 | statusLabel.textContent = message; 211 | }, 212 | showConnectionLink: function(link) { 213 | substatusLabel.querySelector('.link').textContent = link; 214 | qrCode.clear(); 215 | qrCode.makeCode(link); 216 | var hidden = Array.from(substatusLabel.querySelectorAll('[hidden]')); 217 | hidden.forEach(function(element) { 218 | element.hidden = false; 219 | }); 220 | }, 221 | hideConnectionLink: function() { 222 | substatusLabel.querySelector('.center').hidden = true; 223 | }, 224 | addRemoteStream: addRemoteStream, 225 | setLocalStream: function(stream) { 226 | selfView.src = URL.createObjectURL(stream); 227 | } 228 | }; 229 | } 230 | 231 | function withUI(ui) { 232 | return { 233 | addLocalStream: function(stream) { 234 | ui.setLocalStream(stream); 235 | ui.updateStatus('waiting for someone to connect...'); 236 | ui.showConnectionLink(location.href); 237 | }, 238 | updateStatus: function(message) { 239 | if (message.body.type === 'join') { 240 | ui.updateStatus('calling...'); 241 | } else if (message.body.type === 'offer') { 242 | ui.updateStatus('incoming call...'); 243 | } 244 | }, 245 | addRemoteStream: function (stream) { 246 | ui.updateStatus(''); 247 | ui.hideConnectionLink(); 248 | ui.addRemoteStream(stream); 249 | } 250 | }; 251 | } 252 | 253 | var form = document.querySelector('form'); 254 | form.addEventListener('submit', function(e) { 255 | e.preventDefault(); 256 | form.hidden = true; 257 | 258 | var source = Array.from(form.source).find(function(source) { 259 | return source.checked; 260 | }); 261 | 262 | var constraints; 263 | 264 | if (source.dataset.source) { 265 | constraints = Promise.resolve(JSON.parse(source.dataset.source)); 266 | } else { 267 | constraints = window[source.value](); 268 | } 269 | 270 | var mediaDevices = navigator.mediaDevices; 271 | var getUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); 272 | 273 | constraints.then(getUserMedia).then(connect); 274 | }); 275 | 276 | function connect(stream) { 277 | var peers = createMap(stream); 278 | 279 | var signalingChannel = getSignalingChannel(getRoom()); 280 | 281 | // pass events to UI object 282 | var ui = withUI(UI(document)); 283 | ui.addLocalStream(stream); 284 | signalingChannel.messages.forEach(ui.updateStatus); 285 | getRemoteStreams(peers.objects).forEach(ui.addRemoteStream); 286 | 287 | // get responses 288 | var candidates = getCandidates(peers.objects); 289 | var replies = getReplies(signalingChannel.messages, peers); 290 | 291 | candidates.merge(replies).observe(signalingChannel); 292 | } 293 | 294 | function sourceChromeScreen() { 295 | var constraints = { 296 | audio: false, 297 | video: { 298 | mandatory: { 299 | chromeMediaSource: 'desktop', 300 | maxWidth: screen.width > 1920 ? screen.width : 1920, 301 | maxHeight: screen.height > 1080 ? screen.height : 1080 302 | }, 303 | optional: [] 304 | } 305 | }; 306 | var id = Math.random(); 307 | return new Promise(function(resolve, reject) { 308 | window.addEventListener('message', function getScreen(e) { 309 | if (e.data.id === id && e.data.type === 'result' && e.data.command === 'get-sourceid') { 310 | window.removeEventListener('message', getScreen); 311 | constraints.video.mandatory.chromeMediaSourceId = e.data.streamId; 312 | if (e.data.streamId) { 313 | resolve(constraints); 314 | } else { 315 | reject(Error('Missing source parameter')); 316 | } 317 | } 318 | }); 319 | window.postMessage({ 320 | command: 'get-sourceid', 321 | id: id 322 | }, '*'); 323 | }); 324 | } 325 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | body { background-color: #fff; font-family: Arial, sans-serif;} 2 | 3 | .remote { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain} 4 | .status { z-index:1; font-size: xx-large; text-align: center; color: #333; position: absolute; top: 40%; left: 0; width: 100%; object-fit: contain} 5 | .participants { position: absolute; bottom: 10px; right: 10px; left: 10px; height: 20%; text-align: center; } 6 | .participants video { max-width:100%; max-height:100%; box-shadow: 0 0 25px #444; margin-right: 10px; } 7 | 8 | .self { display: none; } 9 | .self[src] { display: inline; } 10 | 11 | .sources { 12 | position: fixed; 13 | top: 50%; 14 | left: 50%; 15 | /* bring your own prefixes */ 16 | transform: translate(-50%, -50%); 17 | z-index: 1; 18 | background: white; 19 | } 20 | .sources label { display: block;} 21 | 22 | .manual { z-index: 1; position: relative; } 23 | .manual textarea { width: 400px; height: 100px;} 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var express = require('express'); 3 | 4 | var WebSocketServer = require('ws').Server; 5 | var app = express(); 6 | 7 | var server; 8 | 9 | if (option('secure')) { 10 | var fs = require('fs'); 11 | var credentials = { 12 | key: fs.readFileSync('cert/server.key', 'utf8'), 13 | cert: fs.readFileSync('cert/server.crt', 'utf8') 14 | }; 15 | server = require('https').createServer(credentials, app); 16 | } else { 17 | server = require('http').createServer(app); 18 | } 19 | 20 | app.use(express.static(__dirname + '/public')); 21 | 22 | function sendToOneClient(clientId, client) { 23 | return client.id === clientId; 24 | } 25 | 26 | function sendToOthers(ownConnection, client) { 27 | return ownConnection.upgradeReq.url === client.upgradeReq.url && 28 | client !== ownConnection; 29 | } 30 | 31 | function parseJSON(data) { 32 | try { 33 | return JSON.parse(data); 34 | } catch (e) { 35 | console.warn('Cannot parse JSON', e); 36 | } 37 | } 38 | 39 | function option(name) { 40 | var env = process.env; 41 | return env['npm_config_' + name] || env[name.toUpperCase()] || env['npm_package_config_' + name]; 42 | } 43 | 44 | var configuration = { 45 | server: server 46 | }; 47 | 48 | if (option('origin')) { 49 | configuration.verifyClient = function(info) { 50 | return info.origin === option('origin'); 51 | }; 52 | } else { 53 | console.warn('Verifying client connections disabled!') 54 | } 55 | 56 | var ws = new WebSocketServer(configuration); 57 | 58 | ws.on('connection', function(connection) { 59 | connection.id = crypto.randomBytes(20).toString('hex'); 60 | ws.clients.filter(sendToOthers.bind(null, connection)).forEach(function(client) { 61 | client.send(JSON.stringify({ 62 | from: connection.id, 63 | body: { 64 | type: 'join' 65 | } 66 | })); 67 | }); 68 | connection.on('message', function(data) { 69 | var message = parseJSON(data); 70 | if (!message) return; 71 | var target = message.to ? sendToOneClient.bind(null, message.to) : sendToOthers.bind(null, connection); 72 | ws.clients.filter(target).forEach(function(client) { 73 | client.send(JSON.stringify({ 74 | from: connection.id, 75 | body: message.body 76 | })); 77 | }); 78 | console.log('received:', message); 79 | }); 80 | }); 81 | 82 | server.listen(option('port')); 83 | 84 | console.info('Open: http' + (option('secure') ? 's' : '') + '://localhost:' + option('port')); 85 | --------------------------------------------------------------------------------