├── .gitignore ├── MANIFEST.in ├── README.MD ├── nginx-config ├── nginx.conf └── webRTC.conf ├── setup.py └── webRTCserver ├── __init__.py ├── static ├── css │ └── main.css └── js │ ├── lib │ ├── adapter.js │ └── socket.io.js │ └── main.js ├── templates └── index.html └── webRTCserver.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | webRTCserver.egg-info 3 | .pyc 4 | webRTCserver/nohup.out 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft webRTCserver/templates 2 | graft webRTCserver/static 3 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Remote connection with flask and eventlet 2 | 3 | ## Getting this app running on your server 4 | 5 | 1. Find some webserver. SSH into it. 6 | 2. Get a domain name and make sure you properly point it to your server. 7 | 3. Install Nginx, python and pip on the server 8 | 4. Clone this repo onto your server. 9 | 5. Run a HTTPS enabled Nginx reverse proxy for the eventlet WSGI server that will run on localhost:8080. You *need* to have HTTPS configured for your your server in order for this to work as WebRTC will not operate on a HTTP protocol. Use these [nginx-config files](https://github.com/nanomosfet/WebRTC-Flask-server/tree/master/nginx-config) as a guide to creating your own server configuration on Nginx for your domain. Also note that I used [Let's Encrypt](https://letsencrypt.org/) to manage my ssl cert. Look at [these Nginx resources](https://www.nginx.com/resources/admin-guide/) for more information on Nginx server configuration. 10 | 6. Change directory into root file of this repository. Run `pip install .` to install all needed packages. 11 | 7. Run the webRTCserver.py file `python webRTCserver.py`. You can check now that on your localhost:8080 that a page is loading. Note that if you are on the web you need to have HTTPS set up. 12 | 13 | 8. Last word I should mention that this has no stun and turn server implementation, only using googles stun server which is not meant for production. More info on [WebRTC in the real world](https://www.html5rocks.com/en/tutorials/webrtc/infrastructure/) 14 | -------------------------------------------------------------------------------- /nginx-config/nginx.conf: -------------------------------------------------------------------------------- 1 | user ubuntu; 2 | worker_processes auto; 3 | pid /run/nginx.pid; 4 | 5 | events { 6 | worker_connections 768; 7 | # multi_accept on; 8 | } 9 | 10 | http { 11 | 12 | ## 13 | # Basic Settings 14 | ## 15 | 16 | sendfile on; 17 | tcp_nopush on; 18 | tcp_nodelay on; 19 | keepalive_timeout 65; 20 | types_hash_max_size 2048; 21 | # server_tokens off; 22 | 23 | # server_names_hash_bucket_size 128; 24 | # server_name_in_redirect off; 25 | 26 | include /etc/nginx/mime.types; 27 | default_type application/octet-stream; 28 | 29 | ## 30 | # SSL Settings 31 | ## 32 | 33 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 34 | ssl_prefer_server_ciphers on; 35 | ssl_session_cache shared:SSL:1m; 36 | ssl_session_timeout 1440m; 37 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; 38 | 39 | ssl_dhparam /etc/certs/dhparams.pem; 40 | 41 | ## 42 | # Logging Settings 43 | ## 44 | 45 | access_log /var/log/nginx/access.log; 46 | error_log /var/log/nginx/error.log; 47 | 48 | ## 49 | # Gzip Settings 50 | ## 51 | 52 | gzip on; 53 | gzip_disable "msie6"; 54 | 55 | # gzip_vary on; 56 | # gzip_proxied any; 57 | # gzip_comp_level 6; 58 | # gzip_buffers 16 8k; 59 | # gzip_http_version 1.1; 60 | # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 61 | 62 | ## 63 | # Virtual Host Configs 64 | ## 65 | 66 | include /etc/nginx/conf.d/*.conf; 67 | include /etc/nginx/sites-enabled/*; 68 | } 69 | 70 | 71 | #mail { 72 | # # See sample authentication script at: 73 | # # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript 74 | # 75 | # # auth_http localhost/auth.php; 76 | # # pop3_capabilities "TOP" "USER"; 77 | # # imap_capabilities "IMAP4rev1" "UIDPLUS"; 78 | # 79 | # server { 80 | # listen localhost:110; 81 | # protocol pop3; 82 | # proxy on; 83 | # } 84 | # 85 | # server { 86 | # listen localhost:143; 87 | # protocol imap; 88 | # proxy on; 89 | # } 90 | #} 91 | -------------------------------------------------------------------------------- /nginx-config/webRTC.conf: -------------------------------------------------------------------------------- 1 | # Virtual Host configuration for example.com 2 | # 3 | # You can move that to a different file under sites-available/ and symlink that 4 | # to sites-enabled/ to enable it. 5 | # 6 | server { 7 | listen 443 ssl; 8 | server_name tim.example.com; 9 | root /var/nginx/webRTCserver; 10 | ssl_certificate /etc/certs/server.crt; 11 | ssl_certificate_key /etc/certs/server.key; 12 | ssl_trusted_certificate /etc/ssl/certs/GeoTrust_Global_CA.pem; 13 | 14 | add_header Strict-Transport-Security max-age=15768000; 15 | 16 | ssl_stapling on; 17 | ssl_stapling_verify on; 18 | 19 | location / { 20 | try_files $uri @webRTCserver; 21 | proxy_redirect off; 22 | proxy_set_header Host $host; 23 | proxy_set_header X-Real-IP $remote_addr; 24 | proxy_set_header X-Forwared-For $proxy_add_x_forwarded_for; 25 | } 26 | 27 | location @webRTCserver { 28 | proxy_pass http://127.0.0.1:8080; 29 | } 30 | 31 | location /socket.io/ { 32 | proxy_set_header Upgrade $http_upgrade; 33 | proxy_set_header Connection "upgrade"; 34 | proxy_http_version 1.1; 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 36 | proxy_set_header Host $host; 37 | proxy_pass http://127.0.0.1:8080; 38 | } 39 | 40 | if ($scheme != "https") { 41 | return 301 https://$host$request_uri; 42 | } # managed by Certbot 43 | 44 | } 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='webRTCserver', 5 | packages=find_packages(), 6 | include_package_data=True, 7 | install_requires=[ 8 | 'flask', 9 | 'python-socketio', 10 | 'eventlet' 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /webRTCserver/__init__.py: -------------------------------------------------------------------------------- 1 | from .webRTCserver import app -------------------------------------------------------------------------------- /webRTCserver/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | video { 6 | max-width: 100%; 7 | width: 320px; 8 | } 9 | -------------------------------------------------------------------------------- /webRTCserver/static/js/lib/adapter.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && typeof selector === 'function') { 205 | return origGetStats(selector, successCallback); 206 | } 207 | 208 | var fixChromeStats_ = function(response) { 209 | var standardReport = {}; 210 | var reports = response.result(); 211 | reports.forEach(function(report) { 212 | var standardStats = { 213 | id: report.id, 214 | timestamp: report.timestamp, 215 | type: report.type 216 | }; 217 | report.names().forEach(function(name) { 218 | standardStats[name] = report.stat(name); 219 | }); 220 | standardReport[standardStats.id] = standardStats; 221 | }); 222 | 223 | return standardReport; 224 | }; 225 | 226 | if (arguments.length >= 2) { 227 | var successCallbackWrapper_ = function(response) { 228 | args[1](fixChromeStats_(response)); 229 | }; 230 | 231 | return origGetStats.apply(this, [successCallbackWrapper_, 232 | arguments[0]]); 233 | } 234 | 235 | // promise-support 236 | return new Promise(function(resolve, reject) { 237 | if (args.length === 1 && typeof selector === 'object') { 238 | origGetStats.apply(self, 239 | [function(response) { 240 | resolve.apply(null, [fixChromeStats_(response)]); 241 | }, reject]); 242 | } else { 243 | origGetStats.apply(self, [resolve, reject]); 244 | } 245 | }); 246 | }; 247 | 248 | return pc; 249 | }; 250 | window.RTCPeerConnection.prototype = webkitRTCPeerConnection.prototype; 251 | 252 | // wrap static methods. Currently just generateCertificate. 253 | if (webkitRTCPeerConnection.generateCertificate) { 254 | Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { 255 | get: function() { 256 | return webkitRTCPeerConnection.generateCertificate; 257 | } 258 | }); 259 | } 260 | 261 | // add promise support 262 | ['createOffer', 'createAnswer'].forEach(function(method) { 263 | var nativeMethod = webkitRTCPeerConnection.prototype[method]; 264 | webkitRTCPeerConnection.prototype[method] = function() { 265 | var self = this; 266 | if (arguments.length < 1 || (arguments.length === 1 && 267 | typeof(arguments[0]) === 'object')) { 268 | var opts = arguments.length === 1 ? arguments[0] : undefined; 269 | return new Promise(function(resolve, reject) { 270 | nativeMethod.apply(self, [resolve, reject, opts]); 271 | }); 272 | } 273 | return nativeMethod.apply(this, arguments); 274 | }; 275 | }); 276 | 277 | ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] 278 | .forEach(function(method) { 279 | var nativeMethod = webkitRTCPeerConnection.prototype[method]; 280 | webkitRTCPeerConnection.prototype[method] = function() { 281 | var args = arguments; 282 | var self = this; 283 | args[0] = new ((method === 'addIceCandidate')? 284 | RTCIceCandidate : RTCSessionDescription)(args[0]); 285 | return new Promise(function(resolve, reject) { 286 | nativeMethod.apply(self, [args[0], 287 | function() { 288 | resolve(); 289 | if (args.length >= 2) { 290 | args[1].apply(null, []); 291 | } 292 | }, 293 | function(err) { 294 | reject(err); 295 | if (args.length >= 3) { 296 | args[2].apply(null, [err]); 297 | } 298 | }] 299 | ); 300 | }); 301 | }; 302 | }); 303 | }, 304 | 305 | // Attach a media stream to an element. 306 | attachMediaStream: function(element, stream) { 307 | logging('DEPRECATED, attachMediaStream will soon be removed.'); 308 | if (browserDetails.version >= 43) { 309 | element.srcObject = stream; 310 | } else if (typeof element.src !== 'undefined') { 311 | element.src = URL.createObjectURL(stream); 312 | } else { 313 | logging('Error attaching stream to element.'); 314 | } 315 | }, 316 | 317 | reattachMediaStream: function(to, from) { 318 | logging('DEPRECATED, reattachMediaStream will soon be removed.'); 319 | if (browserDetails.version >= 43) { 320 | to.srcObject = from.srcObject; 321 | } else { 322 | to.src = from.src; 323 | } 324 | } 325 | }; 326 | 327 | 328 | // Expose public methods. 329 | module.exports = { 330 | shimOnTrack: chromeShim.shimOnTrack, 331 | shimSourceObject: chromeShim.shimSourceObject, 332 | shimPeerConnection: chromeShim.shimPeerConnection, 333 | shimGetUserMedia: require('./getusermedia'), 334 | attachMediaStream: chromeShim.attachMediaStream, 335 | reattachMediaStream: chromeShim.reattachMediaStream 336 | }; 337 | 338 | },{"../utils.js":9,"./getusermedia":3}],3:[function(require,module,exports){ 339 | /* 340 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 341 | * 342 | * Use of this source code is governed by a BSD-style license 343 | * that can be found in the LICENSE file in the root of the source 344 | * tree. 345 | */ 346 | /* eslint-env node */ 347 | 'use strict'; 348 | var logging = require('../utils.js').log; 349 | 350 | // Expose public methods. 351 | module.exports = function() { 352 | var constraintsToChrome_ = function(c) { 353 | if (typeof c !== 'object' || c.mandatory || c.optional) { 354 | return c; 355 | } 356 | var cc = {}; 357 | Object.keys(c).forEach(function(key) { 358 | if (key === 'require' || key === 'advanced' || key === 'mediaSource') { 359 | return; 360 | } 361 | var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; 362 | if (r.exact !== undefined && typeof r.exact === 'number') { 363 | r.min = r.max = r.exact; 364 | } 365 | var oldname_ = function(prefix, name) { 366 | if (prefix) { 367 | return prefix + name.charAt(0).toUpperCase() + name.slice(1); 368 | } 369 | return (name === 'deviceId') ? 'sourceId' : name; 370 | }; 371 | if (r.ideal !== undefined) { 372 | cc.optional = cc.optional || []; 373 | var oc = {}; 374 | if (typeof r.ideal === 'number') { 375 | oc[oldname_('min', key)] = r.ideal; 376 | cc.optional.push(oc); 377 | oc = {}; 378 | oc[oldname_('max', key)] = r.ideal; 379 | cc.optional.push(oc); 380 | } else { 381 | oc[oldname_('', key)] = r.ideal; 382 | cc.optional.push(oc); 383 | } 384 | } 385 | if (r.exact !== undefined && typeof r.exact !== 'number') { 386 | cc.mandatory = cc.mandatory || {}; 387 | cc.mandatory[oldname_('', key)] = r.exact; 388 | } else { 389 | ['min', 'max'].forEach(function(mix) { 390 | if (r[mix] !== undefined) { 391 | cc.mandatory = cc.mandatory || {}; 392 | cc.mandatory[oldname_(mix, key)] = r[mix]; 393 | } 394 | }); 395 | } 396 | }); 397 | if (c.advanced) { 398 | cc.optional = (cc.optional || []).concat(c.advanced); 399 | } 400 | return cc; 401 | }; 402 | 403 | var getUserMedia_ = function(constraints, onSuccess, onError) { 404 | constraints = JSON.parse(JSON.stringify(constraints)); 405 | if (constraints.audio) { 406 | constraints.audio = constraintsToChrome_(constraints.audio); 407 | } 408 | if (constraints.video) { 409 | constraints.video = constraintsToChrome_(constraints.video); 410 | } 411 | logging('chrome: ' + JSON.stringify(constraints)); 412 | return navigator.webkitGetUserMedia(constraints, onSuccess, onError); 413 | }; 414 | navigator.getUserMedia = getUserMedia_; 415 | 416 | // Returns the result of getUserMedia as a Promise. 417 | var getUserMediaPromise_ = function(constraints) { 418 | return new Promise(function(resolve, reject) { 419 | navigator.getUserMedia(constraints, resolve, reject); 420 | }); 421 | }; 422 | 423 | if (!navigator.mediaDevices) { 424 | navigator.mediaDevices = { 425 | getUserMedia: getUserMediaPromise_, 426 | enumerateDevices: function() { 427 | return new Promise(function(resolve) { 428 | var kinds = {audio: 'audioinput', video: 'videoinput'}; 429 | return MediaStreamTrack.getSources(function(devices) { 430 | resolve(devices.map(function(device) { 431 | return {label: device.label, 432 | kind: kinds[device.kind], 433 | deviceId: device.id, 434 | groupId: ''}; 435 | })); 436 | }); 437 | }); 438 | } 439 | }; 440 | } 441 | 442 | // A shim for getUserMedia method on the mediaDevices object. 443 | // TODO(KaptenJansson) remove once implemented in Chrome stable. 444 | if (!navigator.mediaDevices.getUserMedia) { 445 | navigator.mediaDevices.getUserMedia = function(constraints) { 446 | return getUserMediaPromise_(constraints); 447 | }; 448 | } else { 449 | // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia 450 | // function which returns a Promise, it does not accept spec-style 451 | // constraints. 452 | var origGetUserMedia = navigator.mediaDevices.getUserMedia. 453 | bind(navigator.mediaDevices); 454 | navigator.mediaDevices.getUserMedia = function(c) { 455 | if (c) { 456 | logging('spec: ' + JSON.stringify(c)); // whitespace for alignment 457 | c.audio = constraintsToChrome_(c.audio); 458 | c.video = constraintsToChrome_(c.video); 459 | logging('chrome: ' + JSON.stringify(c)); 460 | } 461 | return origGetUserMedia(c); 462 | }.bind(this); 463 | } 464 | 465 | // Dummy devicechange event methods. 466 | // TODO(KaptenJansson) remove once implemented in Chrome stable. 467 | if (typeof navigator.mediaDevices.addEventListener === 'undefined') { 468 | navigator.mediaDevices.addEventListener = function() { 469 | logging('Dummy mediaDevices.addEventListener called.'); 470 | }; 471 | } 472 | if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { 473 | navigator.mediaDevices.removeEventListener = function() { 474 | logging('Dummy mediaDevices.removeEventListener called.'); 475 | }; 476 | } 477 | }; 478 | 479 | },{"../utils.js":9}],4:[function(require,module,exports){ 480 | /* 481 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 482 | * 483 | * Use of this source code is governed by a BSD-style license 484 | * that can be found in the LICENSE file in the root of the source 485 | * tree. 486 | */ 487 | /* eslint-env node */ 488 | 'use strict'; 489 | 490 | // SDP helpers. 491 | var SDPUtils = {}; 492 | 493 | // Generate an alphanumeric identifier for cname or mids. 494 | // TODO: use UUIDs instead? https://gist.github.com/jed/982883 495 | SDPUtils.generateIdentifier = function() { 496 | return Math.random().toString(36).substr(2, 10); 497 | }; 498 | 499 | // The RTCP CNAME used by all peerconnections from the same JS. 500 | SDPUtils.localCName = SDPUtils.generateIdentifier(); 501 | 502 | // Splits SDP into lines, dealing with both CRLF and LF. 503 | SDPUtils.splitLines = function(blob) { 504 | return blob.trim().split('\n').map(function(line) { 505 | return line.trim(); 506 | }); 507 | }; 508 | // Splits SDP into sessionpart and mediasections. Ensures CRLF. 509 | SDPUtils.splitSections = function(blob) { 510 | var parts = blob.split('\nm='); 511 | return parts.map(function(part, index) { 512 | return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; 513 | }); 514 | }; 515 | 516 | // Returns lines that start with a certain prefix. 517 | SDPUtils.matchPrefix = function(blob, prefix) { 518 | return SDPUtils.splitLines(blob).filter(function(line) { 519 | return line.indexOf(prefix) === 0; 520 | }); 521 | }; 522 | 523 | // Parses an ICE candidate line. Sample input: 524 | // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 525 | // rport 55996" 526 | SDPUtils.parseCandidate = function(line) { 527 | var parts; 528 | // Parse both variants. 529 | if (line.indexOf('a=candidate:') === 0) { 530 | parts = line.substring(12).split(' '); 531 | } else { 532 | parts = line.substring(10).split(' '); 533 | } 534 | 535 | var candidate = { 536 | foundation: parts[0], 537 | component: parts[1], 538 | protocol: parts[2].toLowerCase(), 539 | priority: parseInt(parts[3], 10), 540 | ip: parts[4], 541 | port: parseInt(parts[5], 10), 542 | // skip parts[6] == 'typ' 543 | type: parts[7] 544 | }; 545 | 546 | for (var i = 8; i < parts.length; i += 2) { 547 | switch (parts[i]) { 548 | case 'raddr': 549 | candidate.relatedAddress = parts[i + 1]; 550 | break; 551 | case 'rport': 552 | candidate.relatedPort = parseInt(parts[i + 1], 10); 553 | break; 554 | case 'tcptype': 555 | candidate.tcpType = parts[i + 1]; 556 | break; 557 | default: // Unknown extensions are silently ignored. 558 | break; 559 | } 560 | } 561 | return candidate; 562 | }; 563 | 564 | // Translates a candidate object into SDP candidate attribute. 565 | SDPUtils.writeCandidate = function(candidate) { 566 | var sdp = []; 567 | sdp.push(candidate.foundation); 568 | sdp.push(candidate.component); 569 | sdp.push(candidate.protocol.toUpperCase()); 570 | sdp.push(candidate.priority); 571 | sdp.push(candidate.ip); 572 | sdp.push(candidate.port); 573 | 574 | var type = candidate.type; 575 | sdp.push('typ'); 576 | sdp.push(type); 577 | if (type !== 'host' && candidate.relatedAddress && 578 | candidate.relatedPort) { 579 | sdp.push('raddr'); 580 | sdp.push(candidate.relatedAddress); // was: relAddr 581 | sdp.push('rport'); 582 | sdp.push(candidate.relatedPort); // was: relPort 583 | } 584 | if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { 585 | sdp.push('tcptype'); 586 | sdp.push(candidate.tcpType); 587 | } 588 | return 'candidate:' + sdp.join(' '); 589 | }; 590 | 591 | // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: 592 | // a=rtpmap:111 opus/48000/2 593 | SDPUtils.parseRtpMap = function(line) { 594 | var parts = line.substr(9).split(' '); 595 | var parsed = { 596 | payloadType: parseInt(parts.shift(), 10) // was: id 597 | }; 598 | 599 | parts = parts[0].split('/'); 600 | 601 | parsed.name = parts[0]; 602 | parsed.clockRate = parseInt(parts[1], 10); // was: clockrate 603 | // was: channels 604 | parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; 605 | return parsed; 606 | }; 607 | 608 | // Generate an a=rtpmap line from RTCRtpCodecCapability or 609 | // RTCRtpCodecParameters. 610 | SDPUtils.writeRtpMap = function(codec) { 611 | var pt = codec.payloadType; 612 | if (codec.preferredPayloadType !== undefined) { 613 | pt = codec.preferredPayloadType; 614 | } 615 | return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + 616 | (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; 617 | }; 618 | 619 | // Parses an a=extmap line (headerextension from RFC 5285). Sample input: 620 | // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 621 | SDPUtils.parseExtmap = function(line) { 622 | var parts = line.substr(9).split(' '); 623 | return { 624 | id: parseInt(parts[0], 10), 625 | uri: parts[1] 626 | }; 627 | }; 628 | 629 | // Generates a=extmap line from RTCRtpHeaderExtensionParameters or 630 | // RTCRtpHeaderExtension. 631 | SDPUtils.writeExtmap = function(headerExtension) { 632 | return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + 633 | ' ' + headerExtension.uri + '\r\n'; 634 | }; 635 | 636 | // Parses an ftmp line, returns dictionary. Sample input: 637 | // a=fmtp:96 vbr=on;cng=on 638 | // Also deals with vbr=on; cng=on 639 | SDPUtils.parseFmtp = function(line) { 640 | var parsed = {}; 641 | var kv; 642 | var parts = line.substr(line.indexOf(' ') + 1).split(';'); 643 | for (var j = 0; j < parts.length; j++) { 644 | kv = parts[j].trim().split('='); 645 | parsed[kv[0].trim()] = kv[1]; 646 | } 647 | return parsed; 648 | }; 649 | 650 | // Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. 651 | SDPUtils.writeFmtp = function(codec) { 652 | var line = ''; 653 | var pt = codec.payloadType; 654 | if (codec.preferredPayloadType !== undefined) { 655 | pt = codec.preferredPayloadType; 656 | } 657 | if (codec.parameters && Object.keys(codec.parameters).length) { 658 | var params = []; 659 | Object.keys(codec.parameters).forEach(function(param) { 660 | params.push(param + '=' + codec.parameters[param]); 661 | }); 662 | line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; 663 | } 664 | return line; 665 | }; 666 | 667 | // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: 668 | // a=rtcp-fb:98 nack rpsi 669 | SDPUtils.parseRtcpFb = function(line) { 670 | var parts = line.substr(line.indexOf(' ') + 1).split(' '); 671 | return { 672 | type: parts.shift(), 673 | parameter: parts.join(' ') 674 | }; 675 | }; 676 | // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. 677 | SDPUtils.writeRtcpFb = function(codec) { 678 | var lines = ''; 679 | var pt = codec.payloadType; 680 | if (codec.preferredPayloadType !== undefined) { 681 | pt = codec.preferredPayloadType; 682 | } 683 | if (codec.rtcpFeedback && codec.rtcpFeedback.length) { 684 | // FIXME: special handling for trr-int? 685 | codec.rtcpFeedback.forEach(function(fb) { 686 | lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + ' ' + fb.parameter + 687 | '\r\n'; 688 | }); 689 | } 690 | return lines; 691 | }; 692 | 693 | // Parses an RFC 5576 ssrc media attribute. Sample input: 694 | // a=ssrc:3735928559 cname:something 695 | SDPUtils.parseSsrcMedia = function(line) { 696 | var sp = line.indexOf(' '); 697 | var parts = { 698 | ssrc: parseInt(line.substr(7, sp - 7), 10) 699 | }; 700 | var colon = line.indexOf(':', sp); 701 | if (colon > -1) { 702 | parts.attribute = line.substr(sp + 1, colon - sp - 1); 703 | parts.value = line.substr(colon + 1); 704 | } else { 705 | parts.attribute = line.substr(sp + 1); 706 | } 707 | return parts; 708 | }; 709 | 710 | // Extracts DTLS parameters from SDP media section or sessionpart. 711 | // FIXME: for consistency with other functions this should only 712 | // get the fingerprint line as input. See also getIceParameters. 713 | SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { 714 | var lines = SDPUtils.splitLines(mediaSection); 715 | // Search in session part, too. 716 | lines = lines.concat(SDPUtils.splitLines(sessionpart)); 717 | var fpLine = lines.filter(function(line) { 718 | return line.indexOf('a=fingerprint:') === 0; 719 | })[0].substr(14); 720 | // Note: a=setup line is ignored since we use the 'auto' role. 721 | var dtlsParameters = { 722 | role: 'auto', 723 | fingerprints: [{ 724 | algorithm: fpLine.split(' ')[0], 725 | value: fpLine.split(' ')[1] 726 | }] 727 | }; 728 | return dtlsParameters; 729 | }; 730 | 731 | // Serializes DTLS parameters to SDP. 732 | SDPUtils.writeDtlsParameters = function(params, setupType) { 733 | var sdp = 'a=setup:' + setupType + '\r\n'; 734 | params.fingerprints.forEach(function(fp) { 735 | sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; 736 | }); 737 | return sdp; 738 | }; 739 | // Parses ICE information from SDP media section or sessionpart. 740 | // FIXME: for consistency with other functions this should only 741 | // get the ice-ufrag and ice-pwd lines as input. 742 | SDPUtils.getIceParameters = function(mediaSection, sessionpart) { 743 | var lines = SDPUtils.splitLines(mediaSection); 744 | // Search in session part, too. 745 | lines = lines.concat(SDPUtils.splitLines(sessionpart)); 746 | var iceParameters = { 747 | usernameFragment: lines.filter(function(line) { 748 | return line.indexOf('a=ice-ufrag:') === 0; 749 | })[0].substr(12), 750 | password: lines.filter(function(line) { 751 | return line.indexOf('a=ice-pwd:') === 0; 752 | })[0].substr(10) 753 | }; 754 | return iceParameters; 755 | }; 756 | 757 | // Serializes ICE parameters to SDP. 758 | SDPUtils.writeIceParameters = function(params) { 759 | return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 760 | 'a=ice-pwd:' + params.password + '\r\n'; 761 | }; 762 | 763 | // Parses the SDP media section and returns RTCRtpParameters. 764 | SDPUtils.parseRtpParameters = function(mediaSection) { 765 | var description = { 766 | codecs: [], 767 | headerExtensions: [], 768 | fecMechanisms: [], 769 | rtcp: [] 770 | }; 771 | var lines = SDPUtils.splitLines(mediaSection); 772 | var mline = lines[0].split(' '); 773 | for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] 774 | var pt = mline[i]; 775 | var rtpmapline = SDPUtils.matchPrefix( 776 | mediaSection, 'a=rtpmap:' + pt + ' ')[0]; 777 | if (rtpmapline) { 778 | var codec = SDPUtils.parseRtpMap(rtpmapline); 779 | var fmtps = SDPUtils.matchPrefix( 780 | mediaSection, 'a=fmtp:' + pt + ' '); 781 | // Only the first a=fmtp: is considered. 782 | codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; 783 | codec.rtcpFeedback = SDPUtils.matchPrefix( 784 | mediaSection, 'a=rtcp-fb:' + pt + ' ') 785 | .map(SDPUtils.parseRtcpFb); 786 | description.codecs.push(codec); 787 | // parse FEC mechanisms from rtpmap lines. 788 | switch (codec.name.toUpperCase()) { 789 | case 'RED': 790 | case 'ULPFEC': 791 | description.fecMechanisms.push(codec.name.toUpperCase()); 792 | break; 793 | default: // only RED and ULPFEC are recognized as FEC mechanisms. 794 | break; 795 | } 796 | } 797 | } 798 | SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) { 799 | description.headerExtensions.push(SDPUtils.parseExtmap(line)); 800 | }); 801 | // FIXME: parse rtcp. 802 | return description; 803 | }; 804 | 805 | // Generates parts of the SDP media section describing the capabilities / 806 | // parameters. 807 | SDPUtils.writeRtpDescription = function(kind, caps) { 808 | var sdp = ''; 809 | 810 | // Build the mline. 811 | sdp += 'm=' + kind + ' '; 812 | sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. 813 | sdp += ' UDP/TLS/RTP/SAVPF '; 814 | sdp += caps.codecs.map(function(codec) { 815 | if (codec.preferredPayloadType !== undefined) { 816 | return codec.preferredPayloadType; 817 | } 818 | return codec.payloadType; 819 | }).join(' ') + '\r\n'; 820 | 821 | sdp += 'c=IN IP4 0.0.0.0\r\n'; 822 | sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; 823 | 824 | // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. 825 | caps.codecs.forEach(function(codec) { 826 | sdp += SDPUtils.writeRtpMap(codec); 827 | sdp += SDPUtils.writeFmtp(codec); 828 | sdp += SDPUtils.writeRtcpFb(codec); 829 | }); 830 | // FIXME: add headerExtensions, fecMechanismş and rtcp. 831 | sdp += 'a=rtcp-mux\r\n'; 832 | return sdp; 833 | }; 834 | 835 | // Parses the SDP media section and returns an array of 836 | // RTCRtpEncodingParameters. 837 | SDPUtils.parseRtpEncodingParameters = function(mediaSection) { 838 | var encodingParameters = []; 839 | var description = SDPUtils.parseRtpParameters(mediaSection); 840 | var hasRed = description.fecMechanisms.indexOf('RED') !== -1; 841 | var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; 842 | 843 | // filter a=ssrc:... cname:, ignore PlanB-msid 844 | var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') 845 | .map(function(line) { 846 | return SDPUtils.parseSsrcMedia(line); 847 | }) 848 | .filter(function(parts) { 849 | return parts.attribute === 'cname'; 850 | }); 851 | var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; 852 | var secondarySsrc; 853 | 854 | var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') 855 | .map(function(line) { 856 | var parts = line.split(' '); 857 | parts.shift(); 858 | return parts.map(function(part) { 859 | return parseInt(part, 10); 860 | }); 861 | }); 862 | if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { 863 | secondarySsrc = flows[0][1]; 864 | } 865 | 866 | description.codecs.forEach(function(codec) { 867 | if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { 868 | var encParam = { 869 | ssrc: primarySsrc, 870 | codecPayloadType: parseInt(codec.parameters.apt, 10), 871 | rtx: { 872 | ssrc: secondarySsrc 873 | } 874 | }; 875 | encodingParameters.push(encParam); 876 | if (hasRed) { 877 | encParam = JSON.parse(JSON.stringify(encParam)); 878 | encParam.fec = { 879 | ssrc: secondarySsrc, 880 | mechanism: hasUlpfec ? 'red+ulpfec' : 'red' 881 | }; 882 | encodingParameters.push(encParam); 883 | } 884 | } 885 | }); 886 | if (encodingParameters.length === 0 && primarySsrc) { 887 | encodingParameters.push({ 888 | ssrc: primarySsrc 889 | }); 890 | } 891 | 892 | // we support both b=AS and b=TIAS but interpret AS as TIAS. 893 | var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); 894 | if (bandwidth.length) { 895 | if (bandwidth[0].indexOf('b=TIAS:') === 0) { 896 | bandwidth = parseInt(bandwidth[0].substr(7), 10); 897 | } else if (bandwidth[0].indexOf('b=AS:') === 0) { 898 | bandwidth = parseInt(bandwidth[0].substr(5), 10); 899 | } 900 | encodingParameters.forEach(function(params) { 901 | params.maxBitrate = bandwidth; 902 | }); 903 | } 904 | return encodingParameters; 905 | }; 906 | 907 | SDPUtils.writeSessionBoilerplate = function() { 908 | // FIXME: sess-id should be an NTP timestamp. 909 | return 'v=0\r\n' + 910 | 'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' + 911 | 's=-\r\n' + 912 | 't=0 0\r\n'; 913 | }; 914 | 915 | SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { 916 | var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); 917 | 918 | // Map ICE parameters (ufrag, pwd) to SDP. 919 | sdp += SDPUtils.writeIceParameters( 920 | transceiver.iceGatherer.getLocalParameters()); 921 | 922 | // Map DTLS parameters to SDP. 923 | sdp += SDPUtils.writeDtlsParameters( 924 | transceiver.dtlsTransport.getLocalParameters(), 925 | type === 'offer' ? 'actpass' : 'active'); 926 | 927 | sdp += 'a=mid:' + transceiver.mid + '\r\n'; 928 | 929 | if (transceiver.rtpSender && transceiver.rtpReceiver) { 930 | sdp += 'a=sendrecv\r\n'; 931 | } else if (transceiver.rtpSender) { 932 | sdp += 'a=sendonly\r\n'; 933 | } else if (transceiver.rtpReceiver) { 934 | sdp += 'a=recvonly\r\n'; 935 | } else { 936 | sdp += 'a=inactive\r\n'; 937 | } 938 | 939 | // FIXME: for RTX there might be multiple SSRCs. Not implemented in Edge yet. 940 | if (transceiver.rtpSender) { 941 | var msid = 'msid:' + stream.id + ' ' + 942 | transceiver.rtpSender.track.id + '\r\n'; 943 | sdp += 'a=' + msid; 944 | sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + 945 | ' ' + msid; 946 | } 947 | // FIXME: this should be written by writeRtpDescription. 948 | sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + 949 | ' cname:' + SDPUtils.localCName + '\r\n'; 950 | return sdp; 951 | }; 952 | 953 | // Gets the direction from the mediaSection or the sessionpart. 954 | SDPUtils.getDirection = function(mediaSection, sessionpart) { 955 | // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. 956 | var lines = SDPUtils.splitLines(mediaSection); 957 | for (var i = 0; i < lines.length; i++) { 958 | switch (lines[i]) { 959 | case 'a=sendrecv': 960 | case 'a=sendonly': 961 | case 'a=recvonly': 962 | case 'a=inactive': 963 | return lines[i].substr(2); 964 | default: 965 | // FIXME: What should happen here? 966 | } 967 | } 968 | if (sessionpart) { 969 | return SDPUtils.getDirection(sessionpart); 970 | } 971 | return 'sendrecv'; 972 | }; 973 | 974 | // Expose public methods. 975 | module.exports = SDPUtils; 976 | 977 | },{}],5:[function(require,module,exports){ 978 | /* 979 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 980 | * 981 | * Use of this source code is governed by a BSD-style license 982 | * that can be found in the LICENSE file in the root of the source 983 | * tree. 984 | */ 985 | /* eslint-env node */ 986 | 'use strict'; 987 | 988 | var SDPUtils = require('./edge_sdp'); 989 | var logging = require('../utils').log; 990 | 991 | var edgeShim = { 992 | shimPeerConnection: function() { 993 | if (window.RTCIceGatherer) { 994 | // ORTC defines an RTCIceCandidate object but no constructor. 995 | // Not implemented in Edge. 996 | if (!window.RTCIceCandidate) { 997 | window.RTCIceCandidate = function(args) { 998 | return args; 999 | }; 1000 | } 1001 | // ORTC does not have a session description object but 1002 | // other browsers (i.e. Chrome) that will support both PC and ORTC 1003 | // in the future might have this defined already. 1004 | if (!window.RTCSessionDescription) { 1005 | window.RTCSessionDescription = function(args) { 1006 | return args; 1007 | }; 1008 | } 1009 | } 1010 | 1011 | window.RTCPeerConnection = function(config) { 1012 | var self = this; 1013 | 1014 | var _eventTarget = document.createDocumentFragment(); 1015 | ['addEventListener', 'removeEventListener', 'dispatchEvent'] 1016 | .forEach(function(method) { 1017 | self[method] = _eventTarget[method].bind(_eventTarget); 1018 | }); 1019 | 1020 | this.onicecandidate = null; 1021 | this.onaddstream = null; 1022 | this.ontrack = null; 1023 | this.onremovestream = null; 1024 | this.onsignalingstatechange = null; 1025 | this.oniceconnectionstatechange = null; 1026 | this.onnegotiationneeded = null; 1027 | this.ondatachannel = null; 1028 | 1029 | this.localStreams = []; 1030 | this.remoteStreams = []; 1031 | this.getLocalStreams = function() { 1032 | return self.localStreams; 1033 | }; 1034 | this.getRemoteStreams = function() { 1035 | return self.remoteStreams; 1036 | }; 1037 | 1038 | this.localDescription = new RTCSessionDescription({ 1039 | type: '', 1040 | sdp: '' 1041 | }); 1042 | this.remoteDescription = new RTCSessionDescription({ 1043 | type: '', 1044 | sdp: '' 1045 | }); 1046 | this.signalingState = 'stable'; 1047 | this.iceConnectionState = 'new'; 1048 | this.iceGatheringState = 'new'; 1049 | 1050 | this.iceOptions = { 1051 | gatherPolicy: 'all', 1052 | iceServers: [] 1053 | }; 1054 | if (config && config.iceTransportPolicy) { 1055 | switch (config.iceTransportPolicy) { 1056 | case 'all': 1057 | case 'relay': 1058 | this.iceOptions.gatherPolicy = config.iceTransportPolicy; 1059 | break; 1060 | case 'none': 1061 | // FIXME: remove once implementation and spec have added this. 1062 | throw new TypeError('iceTransportPolicy "none" not supported'); 1063 | default: 1064 | // don't set iceTransportPolicy. 1065 | break; 1066 | } 1067 | } 1068 | if (config && config.iceServers) { 1069 | // Edge does not like 1070 | // 1) stun: 1071 | // 2) turn: that does not have all of turn:host:port?transport=udp 1072 | this.iceOptions.iceServers = config.iceServers.filter(function(server) { 1073 | if (server && server.urls) { 1074 | server.urls = server.urls.filter(function(url) { 1075 | return url.indexOf('turn:') === 0 && 1076 | url.indexOf('transport=udp') !== -1; 1077 | })[0]; 1078 | return !!server.urls; 1079 | } 1080 | return false; 1081 | }); 1082 | } 1083 | 1084 | // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... 1085 | // everything that is needed to describe a SDP m-line. 1086 | this.transceivers = []; 1087 | 1088 | // since the iceGatherer is currently created in createOffer but we 1089 | // must not emit candidates until after setLocalDescription we buffer 1090 | // them in this array. 1091 | this._localIceCandidatesBuffer = []; 1092 | }; 1093 | 1094 | window.RTCPeerConnection.prototype._emitBufferedCandidates = function() { 1095 | var self = this; 1096 | var sections = SDPUtils.splitSections(self.localDescription.sdp); 1097 | // FIXME: need to apply ice candidates in a way which is async but 1098 | // in-order 1099 | this._localIceCandidatesBuffer.forEach(function(event) { 1100 | var end = !event.candidate || Object.keys(event.candidate).length === 0; 1101 | if (end) { 1102 | for (var j = 1; j < sections.length; j++) { 1103 | if (sections[j].indexOf('\r\na=end-of-candidates\r\n') === -1) { 1104 | sections[j] += 'a=end-of-candidates\r\n'; 1105 | } 1106 | } 1107 | } else if (event.candidate.candidate.indexOf('typ endOfCandidates') 1108 | === -1) { 1109 | sections[event.candidate.sdpMLineIndex + 1] += 1110 | 'a=' + event.candidate.candidate + '\r\n'; 1111 | } 1112 | self.localDescription.sdp = sections.join(''); 1113 | self.dispatchEvent(event); 1114 | if (self.onicecandidate !== null) { 1115 | self.onicecandidate(event); 1116 | } 1117 | if (!event.candidate && self.iceGatheringState !== 'complete') { 1118 | var complete = self.transceivers.every(function(transceiver) { 1119 | return transceiver.iceGatherer && 1120 | transceiver.iceGatherer.state === 'completed'; 1121 | }); 1122 | if (complete) { 1123 | self.iceGatheringState = 'complete'; 1124 | } 1125 | } 1126 | }); 1127 | this._localIceCandidatesBuffer = []; 1128 | }; 1129 | 1130 | window.RTCPeerConnection.prototype.addStream = function(stream) { 1131 | // Clone is necessary for local demos mostly, attaching directly 1132 | // to two different senders does not work (build 10547). 1133 | this.localStreams.push(stream.clone()); 1134 | this._maybeFireNegotiationNeeded(); 1135 | }; 1136 | 1137 | window.RTCPeerConnection.prototype.removeStream = function(stream) { 1138 | var idx = this.localStreams.indexOf(stream); 1139 | if (idx > -1) { 1140 | this.localStreams.splice(idx, 1); 1141 | this._maybeFireNegotiationNeeded(); 1142 | } 1143 | }; 1144 | 1145 | // Determines the intersection of local and remote capabilities. 1146 | window.RTCPeerConnection.prototype._getCommonCapabilities = 1147 | function(localCapabilities, remoteCapabilities) { 1148 | var commonCapabilities = { 1149 | codecs: [], 1150 | headerExtensions: [], 1151 | fecMechanisms: [] 1152 | }; 1153 | localCapabilities.codecs.forEach(function(lCodec) { 1154 | for (var i = 0; i < remoteCapabilities.codecs.length; i++) { 1155 | var rCodec = remoteCapabilities.codecs[i]; 1156 | if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && 1157 | lCodec.clockRate === rCodec.clockRate && 1158 | lCodec.numChannels === rCodec.numChannels) { 1159 | // push rCodec so we reply with offerer payload type 1160 | commonCapabilities.codecs.push(rCodec); 1161 | 1162 | // FIXME: also need to determine intersection between 1163 | // .rtcpFeedback and .parameters 1164 | break; 1165 | } 1166 | } 1167 | }); 1168 | 1169 | localCapabilities.headerExtensions 1170 | .forEach(function(lHeaderExtension) { 1171 | for (var i = 0; i < remoteCapabilities.headerExtensions.length; 1172 | i++) { 1173 | var rHeaderExtension = remoteCapabilities.headerExtensions[i]; 1174 | if (lHeaderExtension.uri === rHeaderExtension.uri) { 1175 | commonCapabilities.headerExtensions.push(rHeaderExtension); 1176 | break; 1177 | } 1178 | } 1179 | }); 1180 | 1181 | // FIXME: fecMechanisms 1182 | return commonCapabilities; 1183 | }; 1184 | 1185 | // Create ICE gatherer, ICE transport and DTLS transport. 1186 | window.RTCPeerConnection.prototype._createIceAndDtlsTransports = 1187 | function(mid, sdpMLineIndex) { 1188 | var self = this; 1189 | var iceGatherer = new RTCIceGatherer(self.iceOptions); 1190 | var iceTransport = new RTCIceTransport(iceGatherer); 1191 | iceGatherer.onlocalcandidate = function(evt) { 1192 | var event = new Event('icecandidate'); 1193 | event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; 1194 | 1195 | var cand = evt.candidate; 1196 | var end = !cand || Object.keys(cand).length === 0; 1197 | // Edge emits an empty object for RTCIceCandidateComplete‥ 1198 | if (end) { 1199 | // polyfill since RTCIceGatherer.state is not implemented in 1200 | // Edge 10547 yet. 1201 | if (iceGatherer.state === undefined) { 1202 | iceGatherer.state = 'completed'; 1203 | } 1204 | 1205 | // Emit a candidate with type endOfCandidates to make the samples 1206 | // work. Edge requires addIceCandidate with this empty candidate 1207 | // to start checking. The real solution is to signal 1208 | // end-of-candidates to the other side when getting the null 1209 | // candidate but some apps (like the samples) don't do that. 1210 | event.candidate.candidate = 1211 | 'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates'; 1212 | } else { 1213 | // RTCIceCandidate doesn't have a component, needs to be added 1214 | cand.component = iceTransport.component === 'RTCP' ? 2 : 1; 1215 | event.candidate.candidate = SDPUtils.writeCandidate(cand); 1216 | } 1217 | 1218 | var complete = self.transceivers.every(function(transceiver) { 1219 | return transceiver.iceGatherer && 1220 | transceiver.iceGatherer.state === 'completed'; 1221 | }); 1222 | 1223 | // Emit candidate if localDescription is set. 1224 | // Also emits null candidate when all gatherers are complete. 1225 | switch (self.iceGatheringState) { 1226 | case 'new': 1227 | self._localIceCandidatesBuffer.push(event); 1228 | if (end && complete) { 1229 | self._localIceCandidatesBuffer.push( 1230 | new Event('icecandidate')); 1231 | } 1232 | break; 1233 | case 'gathering': 1234 | self._emitBufferedCandidates(); 1235 | self.dispatchEvent(event); 1236 | if (self.onicecandidate !== null) { 1237 | self.onicecandidate(event); 1238 | } 1239 | if (complete) { 1240 | self.dispatchEvent(new Event('icecandidate')); 1241 | if (self.onicecandidate !== null) { 1242 | self.onicecandidate(new Event('icecandidate')); 1243 | } 1244 | self.iceGatheringState = 'complete'; 1245 | } 1246 | break; 1247 | case 'complete': 1248 | // should not happen... currently! 1249 | break; 1250 | default: // no-op. 1251 | break; 1252 | } 1253 | }; 1254 | iceTransport.onicestatechange = function() { 1255 | self._updateConnectionState(); 1256 | }; 1257 | 1258 | var dtlsTransport = new RTCDtlsTransport(iceTransport); 1259 | dtlsTransport.ondtlsstatechange = function() { 1260 | self._updateConnectionState(); 1261 | }; 1262 | dtlsTransport.onerror = function() { 1263 | // onerror does not set state to failed by itself. 1264 | dtlsTransport.state = 'failed'; 1265 | self._updateConnectionState(); 1266 | }; 1267 | 1268 | return { 1269 | iceGatherer: iceGatherer, 1270 | iceTransport: iceTransport, 1271 | dtlsTransport: dtlsTransport 1272 | }; 1273 | }; 1274 | 1275 | // Start the RTP Sender and Receiver for a transceiver. 1276 | window.RTCPeerConnection.prototype._transceive = function(transceiver, 1277 | send, recv) { 1278 | var params = this._getCommonCapabilities(transceiver.localCapabilities, 1279 | transceiver.remoteCapabilities); 1280 | if (send && transceiver.rtpSender) { 1281 | params.encodings = transceiver.sendEncodingParameters; 1282 | params.rtcp = { 1283 | cname: SDPUtils.localCName 1284 | }; 1285 | if (transceiver.recvEncodingParameters.length) { 1286 | params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc; 1287 | } 1288 | transceiver.rtpSender.send(params); 1289 | } 1290 | if (recv && transceiver.rtpReceiver) { 1291 | params.encodings = transceiver.recvEncodingParameters; 1292 | params.rtcp = { 1293 | cname: transceiver.cname 1294 | }; 1295 | if (transceiver.sendEncodingParameters.length) { 1296 | params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc; 1297 | } 1298 | transceiver.rtpReceiver.receive(params); 1299 | } 1300 | }; 1301 | 1302 | window.RTCPeerConnection.prototype.setLocalDescription = 1303 | function(description) { 1304 | var self = this; 1305 | var sections; 1306 | var sessionpart; 1307 | if (description.type === 'offer') { 1308 | // FIXME: What was the purpose of this empty if statement? 1309 | // if (!this._pendingOffer) { 1310 | // } else { 1311 | if (this._pendingOffer) { 1312 | // VERY limited support for SDP munging. Limited to: 1313 | // * changing the order of codecs 1314 | sections = SDPUtils.splitSections(description.sdp); 1315 | sessionpart = sections.shift(); 1316 | sections.forEach(function(mediaSection, sdpMLineIndex) { 1317 | var caps = SDPUtils.parseRtpParameters(mediaSection); 1318 | self._pendingOffer[sdpMLineIndex].localCapabilities = caps; 1319 | }); 1320 | this.transceivers = this._pendingOffer; 1321 | delete this._pendingOffer; 1322 | } 1323 | } else if (description.type === 'answer') { 1324 | sections = SDPUtils.splitSections(self.remoteDescription.sdp); 1325 | sessionpart = sections.shift(); 1326 | sections.forEach(function(mediaSection, sdpMLineIndex) { 1327 | var transceiver = self.transceivers[sdpMLineIndex]; 1328 | var iceGatherer = transceiver.iceGatherer; 1329 | var iceTransport = transceiver.iceTransport; 1330 | var dtlsTransport = transceiver.dtlsTransport; 1331 | var localCapabilities = transceiver.localCapabilities; 1332 | var remoteCapabilities = transceiver.remoteCapabilities; 1333 | var rejected = mediaSection.split('\n', 1)[0] 1334 | .split(' ', 2)[1] === '0'; 1335 | 1336 | if (!rejected) { 1337 | var remoteIceParameters = SDPUtils.getIceParameters( 1338 | mediaSection, sessionpart); 1339 | iceTransport.start(iceGatherer, remoteIceParameters, 1340 | 'controlled'); 1341 | 1342 | var remoteDtlsParameters = SDPUtils.getDtlsParameters( 1343 | mediaSection, sessionpart); 1344 | dtlsTransport.start(remoteDtlsParameters); 1345 | 1346 | // Calculate intersection of capabilities. 1347 | var params = self._getCommonCapabilities(localCapabilities, 1348 | remoteCapabilities); 1349 | 1350 | // Start the RTCRtpSender. The RTCRtpReceiver for this 1351 | // transceiver has already been started in setRemoteDescription. 1352 | self._transceive(transceiver, 1353 | params.codecs.length > 0, 1354 | false); 1355 | } 1356 | }); 1357 | } 1358 | 1359 | this.localDescription = { 1360 | type: description.type, 1361 | sdp: description.sdp 1362 | }; 1363 | switch (description.type) { 1364 | case 'offer': 1365 | this._updateSignalingState('have-local-offer'); 1366 | break; 1367 | case 'answer': 1368 | this._updateSignalingState('stable'); 1369 | break; 1370 | default: 1371 | throw new TypeError('unsupported type "' + description.type + 1372 | '"'); 1373 | } 1374 | 1375 | // If a success callback was provided, emit ICE candidates after it 1376 | // has been executed. Otherwise, emit callback after the Promise is 1377 | // resolved. 1378 | var hasCallback = arguments.length > 1 && 1379 | typeof arguments[1] === 'function'; 1380 | if (hasCallback) { 1381 | var cb = arguments[1]; 1382 | window.setTimeout(function() { 1383 | cb(); 1384 | if (self.iceGatheringState === 'new') { 1385 | self.iceGatheringState = 'gathering'; 1386 | } 1387 | self._emitBufferedCandidates(); 1388 | }, 0); 1389 | } 1390 | var p = Promise.resolve(); 1391 | p.then(function() { 1392 | if (!hasCallback) { 1393 | if (self.iceGatheringState === 'new') { 1394 | self.iceGatheringState = 'gathering'; 1395 | } 1396 | // Usually candidates will be emitted earlier. 1397 | window.setTimeout(self._emitBufferedCandidates.bind(self), 500); 1398 | } 1399 | }); 1400 | return p; 1401 | }; 1402 | 1403 | window.RTCPeerConnection.prototype.setRemoteDescription = 1404 | function(description) { 1405 | var self = this; 1406 | var stream = new MediaStream(); 1407 | var receiverList = []; 1408 | var sections = SDPUtils.splitSections(description.sdp); 1409 | var sessionpart = sections.shift(); 1410 | sections.forEach(function(mediaSection, sdpMLineIndex) { 1411 | var lines = SDPUtils.splitLines(mediaSection); 1412 | var mline = lines[0].substr(2).split(' '); 1413 | var kind = mline[0]; 1414 | var rejected = mline[1] === '0'; 1415 | var direction = SDPUtils.getDirection(mediaSection, sessionpart); 1416 | 1417 | var transceiver; 1418 | var iceGatherer; 1419 | var iceTransport; 1420 | var dtlsTransport; 1421 | var rtpSender; 1422 | var rtpReceiver; 1423 | var sendEncodingParameters; 1424 | var recvEncodingParameters; 1425 | var localCapabilities; 1426 | 1427 | var track; 1428 | // FIXME: ensure the mediaSection has rtcp-mux set. 1429 | var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection); 1430 | var remoteIceParameters; 1431 | var remoteDtlsParameters; 1432 | if (!rejected) { 1433 | remoteIceParameters = SDPUtils.getIceParameters(mediaSection, 1434 | sessionpart); 1435 | remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, 1436 | sessionpart); 1437 | } 1438 | recvEncodingParameters = 1439 | SDPUtils.parseRtpEncodingParameters(mediaSection); 1440 | 1441 | var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:'); 1442 | if (mid.length) { 1443 | mid = mid[0].substr(6); 1444 | } else { 1445 | mid = SDPUtils.generateIdentifier(); 1446 | } 1447 | 1448 | var cname; 1449 | // Gets the first SSRC. Note that with RTX there might be multiple 1450 | // SSRCs. 1451 | var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') 1452 | .map(function(line) { 1453 | return SDPUtils.parseSsrcMedia(line); 1454 | }) 1455 | .filter(function(obj) { 1456 | return obj.attribute === 'cname'; 1457 | })[0]; 1458 | if (remoteSsrc) { 1459 | cname = remoteSsrc.value; 1460 | } 1461 | 1462 | var isComplete = SDPUtils.matchPrefix(mediaSection, 1463 | 'a=end-of-candidates').length > 0; 1464 | var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:') 1465 | .map(function(cand) { 1466 | return SDPUtils.parseCandidate(cand); 1467 | }) 1468 | .filter(function(cand) { 1469 | return cand.component === '1'; 1470 | }); 1471 | if (description.type === 'offer' && !rejected) { 1472 | var transports = self._createIceAndDtlsTransports(mid, 1473 | sdpMLineIndex); 1474 | if (isComplete) { 1475 | transports.iceTransport.setRemoteCandidates(cands); 1476 | } 1477 | 1478 | localCapabilities = RTCRtpReceiver.getCapabilities(kind); 1479 | sendEncodingParameters = [{ 1480 | ssrc: (2 * sdpMLineIndex + 2) * 1001 1481 | }]; 1482 | 1483 | rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); 1484 | 1485 | track = rtpReceiver.track; 1486 | receiverList.push([track, rtpReceiver]); 1487 | // FIXME: not correct when there are multiple streams but that is 1488 | // not currently supported in this shim. 1489 | stream.addTrack(track); 1490 | 1491 | // FIXME: look at direction. 1492 | if (self.localStreams.length > 0 && 1493 | self.localStreams[0].getTracks().length >= sdpMLineIndex) { 1494 | // FIXME: actually more complicated, needs to match types etc 1495 | var localtrack = self.localStreams[0] 1496 | .getTracks()[sdpMLineIndex]; 1497 | rtpSender = new RTCRtpSender(localtrack, 1498 | transports.dtlsTransport); 1499 | } 1500 | 1501 | self.transceivers[sdpMLineIndex] = { 1502 | iceGatherer: transports.iceGatherer, 1503 | iceTransport: transports.iceTransport, 1504 | dtlsTransport: transports.dtlsTransport, 1505 | localCapabilities: localCapabilities, 1506 | remoteCapabilities: remoteCapabilities, 1507 | rtpSender: rtpSender, 1508 | rtpReceiver: rtpReceiver, 1509 | kind: kind, 1510 | mid: mid, 1511 | cname: cname, 1512 | sendEncodingParameters: sendEncodingParameters, 1513 | recvEncodingParameters: recvEncodingParameters 1514 | }; 1515 | // Start the RTCRtpReceiver now. The RTPSender is started in 1516 | // setLocalDescription. 1517 | self._transceive(self.transceivers[sdpMLineIndex], 1518 | false, 1519 | direction === 'sendrecv' || direction === 'sendonly'); 1520 | } else if (description.type === 'answer' && !rejected) { 1521 | transceiver = self.transceivers[sdpMLineIndex]; 1522 | iceGatherer = transceiver.iceGatherer; 1523 | iceTransport = transceiver.iceTransport; 1524 | dtlsTransport = transceiver.dtlsTransport; 1525 | rtpSender = transceiver.rtpSender; 1526 | rtpReceiver = transceiver.rtpReceiver; 1527 | sendEncodingParameters = transceiver.sendEncodingParameters; 1528 | localCapabilities = transceiver.localCapabilities; 1529 | 1530 | self.transceivers[sdpMLineIndex].recvEncodingParameters = 1531 | recvEncodingParameters; 1532 | self.transceivers[sdpMLineIndex].remoteCapabilities = 1533 | remoteCapabilities; 1534 | self.transceivers[sdpMLineIndex].cname = cname; 1535 | 1536 | if (isComplete) { 1537 | iceTransport.setRemoteCandidates(cands); 1538 | } 1539 | iceTransport.start(iceGatherer, remoteIceParameters, 1540 | 'controlling'); 1541 | dtlsTransport.start(remoteDtlsParameters); 1542 | 1543 | self._transceive(transceiver, 1544 | direction === 'sendrecv' || direction === 'recvonly', 1545 | direction === 'sendrecv' || direction === 'sendonly'); 1546 | 1547 | if (rtpReceiver && 1548 | (direction === 'sendrecv' || direction === 'sendonly')) { 1549 | track = rtpReceiver.track; 1550 | receiverList.push([track, rtpReceiver]); 1551 | stream.addTrack(track); 1552 | } else { 1553 | // FIXME: actually the receiver should be created later. 1554 | delete transceiver.rtpReceiver; 1555 | } 1556 | } 1557 | }); 1558 | 1559 | this.remoteDescription = { 1560 | type: description.type, 1561 | sdp: description.sdp 1562 | }; 1563 | switch (description.type) { 1564 | case 'offer': 1565 | this._updateSignalingState('have-remote-offer'); 1566 | break; 1567 | case 'answer': 1568 | this._updateSignalingState('stable'); 1569 | break; 1570 | default: 1571 | throw new TypeError('unsupported type "' + description.type + 1572 | '"'); 1573 | } 1574 | if (stream.getTracks().length) { 1575 | self.remoteStreams.push(stream); 1576 | window.setTimeout(function() { 1577 | var event = new Event('addstream'); 1578 | event.stream = stream; 1579 | self.dispatchEvent(event); 1580 | if (self.onaddstream !== null) { 1581 | window.setTimeout(function() { 1582 | self.onaddstream(event); 1583 | }, 0); 1584 | } 1585 | 1586 | receiverList.forEach(function(item) { 1587 | var track = item[0]; 1588 | var receiver = item[1]; 1589 | var trackEvent = new Event('track'); 1590 | trackEvent.track = track; 1591 | trackEvent.receiver = receiver; 1592 | trackEvent.streams = [stream]; 1593 | self.dispatchEvent(event); 1594 | if (self.ontrack !== null) { 1595 | window.setTimeout(function() { 1596 | self.ontrack(trackEvent); 1597 | }, 0); 1598 | } 1599 | }); 1600 | }, 0); 1601 | } 1602 | if (arguments.length > 1 && typeof arguments[1] === 'function') { 1603 | window.setTimeout(arguments[1], 0); 1604 | } 1605 | return Promise.resolve(); 1606 | }; 1607 | 1608 | window.RTCPeerConnection.prototype.close = function() { 1609 | this.transceivers.forEach(function(transceiver) { 1610 | /* not yet 1611 | if (transceiver.iceGatherer) { 1612 | transceiver.iceGatherer.close(); 1613 | } 1614 | */ 1615 | if (transceiver.iceTransport) { 1616 | transceiver.iceTransport.stop(); 1617 | } 1618 | if (transceiver.dtlsTransport) { 1619 | transceiver.dtlsTransport.stop(); 1620 | } 1621 | if (transceiver.rtpSender) { 1622 | transceiver.rtpSender.stop(); 1623 | } 1624 | if (transceiver.rtpReceiver) { 1625 | transceiver.rtpReceiver.stop(); 1626 | } 1627 | }); 1628 | // FIXME: clean up tracks, local streams, remote streams, etc 1629 | this._updateSignalingState('closed'); 1630 | }; 1631 | 1632 | // Update the signaling state. 1633 | window.RTCPeerConnection.prototype._updateSignalingState = 1634 | function(newState) { 1635 | this.signalingState = newState; 1636 | var event = new Event('signalingstatechange'); 1637 | this.dispatchEvent(event); 1638 | if (this.onsignalingstatechange !== null) { 1639 | this.onsignalingstatechange(event); 1640 | } 1641 | }; 1642 | 1643 | // Determine whether to fire the negotiationneeded event. 1644 | window.RTCPeerConnection.prototype._maybeFireNegotiationNeeded = 1645 | function() { 1646 | // Fire away (for now). 1647 | var event = new Event('negotiationneeded'); 1648 | this.dispatchEvent(event); 1649 | if (this.onnegotiationneeded !== null) { 1650 | this.onnegotiationneeded(event); 1651 | } 1652 | }; 1653 | 1654 | // Update the connection state. 1655 | window.RTCPeerConnection.prototype._updateConnectionState = function() { 1656 | var self = this; 1657 | var newState; 1658 | var states = { 1659 | 'new': 0, 1660 | closed: 0, 1661 | connecting: 0, 1662 | checking: 0, 1663 | connected: 0, 1664 | completed: 0, 1665 | failed: 0 1666 | }; 1667 | this.transceivers.forEach(function(transceiver) { 1668 | states[transceiver.iceTransport.state]++; 1669 | states[transceiver.dtlsTransport.state]++; 1670 | }); 1671 | // ICETransport.completed and connected are the same for this purpose. 1672 | states.connected += states.completed; 1673 | 1674 | newState = 'new'; 1675 | if (states.failed > 0) { 1676 | newState = 'failed'; 1677 | } else if (states.connecting > 0 || states.checking > 0) { 1678 | newState = 'connecting'; 1679 | } else if (states.disconnected > 0) { 1680 | newState = 'disconnected'; 1681 | } else if (states.new > 0) { 1682 | newState = 'new'; 1683 | } else if (states.connected > 0 || states.completed > 0) { 1684 | newState = 'connected'; 1685 | } 1686 | 1687 | if (newState !== self.iceConnectionState) { 1688 | self.iceConnectionState = newState; 1689 | var event = new Event('iceconnectionstatechange'); 1690 | this.dispatchEvent(event); 1691 | if (this.oniceconnectionstatechange !== null) { 1692 | this.oniceconnectionstatechange(event); 1693 | } 1694 | } 1695 | }; 1696 | 1697 | window.RTCPeerConnection.prototype.createOffer = function() { 1698 | var self = this; 1699 | if (this._pendingOffer) { 1700 | throw new Error('createOffer called while there is a pending offer.'); 1701 | } 1702 | var offerOptions; 1703 | if (arguments.length === 1 && typeof arguments[0] !== 'function') { 1704 | offerOptions = arguments[0]; 1705 | } else if (arguments.length === 3) { 1706 | offerOptions = arguments[2]; 1707 | } 1708 | 1709 | var tracks = []; 1710 | var numAudioTracks = 0; 1711 | var numVideoTracks = 0; 1712 | // Default to sendrecv. 1713 | if (this.localStreams.length) { 1714 | numAudioTracks = this.localStreams[0].getAudioTracks().length; 1715 | numVideoTracks = this.localStreams[0].getVideoTracks().length; 1716 | } 1717 | // Determine number of audio and video tracks we need to send/recv. 1718 | if (offerOptions) { 1719 | // Reject Chrome legacy constraints. 1720 | if (offerOptions.mandatory || offerOptions.optional) { 1721 | throw new TypeError( 1722 | 'Legacy mandatory/optional constraints not supported.'); 1723 | } 1724 | if (offerOptions.offerToReceiveAudio !== undefined) { 1725 | numAudioTracks = offerOptions.offerToReceiveAudio; 1726 | } 1727 | if (offerOptions.offerToReceiveVideo !== undefined) { 1728 | numVideoTracks = offerOptions.offerToReceiveVideo; 1729 | } 1730 | } 1731 | if (this.localStreams.length) { 1732 | // Push local streams. 1733 | this.localStreams[0].getTracks().forEach(function(track) { 1734 | tracks.push({ 1735 | kind: track.kind, 1736 | track: track, 1737 | wantReceive: track.kind === 'audio' ? 1738 | numAudioTracks > 0 : numVideoTracks > 0 1739 | }); 1740 | if (track.kind === 'audio') { 1741 | numAudioTracks--; 1742 | } else if (track.kind === 'video') { 1743 | numVideoTracks--; 1744 | } 1745 | }); 1746 | } 1747 | // Create M-lines for recvonly streams. 1748 | while (numAudioTracks > 0 || numVideoTracks > 0) { 1749 | if (numAudioTracks > 0) { 1750 | tracks.push({ 1751 | kind: 'audio', 1752 | wantReceive: true 1753 | }); 1754 | numAudioTracks--; 1755 | } 1756 | if (numVideoTracks > 0) { 1757 | tracks.push({ 1758 | kind: 'video', 1759 | wantReceive: true 1760 | }); 1761 | numVideoTracks--; 1762 | } 1763 | } 1764 | 1765 | var sdp = SDPUtils.writeSessionBoilerplate(); 1766 | var transceivers = []; 1767 | tracks.forEach(function(mline, sdpMLineIndex) { 1768 | // For each track, create an ice gatherer, ice transport, 1769 | // dtls transport, potentially rtpsender and rtpreceiver. 1770 | var track = mline.track; 1771 | var kind = mline.kind; 1772 | var mid = SDPUtils.generateIdentifier(); 1773 | 1774 | var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex); 1775 | 1776 | var localCapabilities = RTCRtpSender.getCapabilities(kind); 1777 | var rtpSender; 1778 | var rtpReceiver; 1779 | 1780 | // generate an ssrc now, to be used later in rtpSender.send 1781 | var sendEncodingParameters = [{ 1782 | ssrc: (2 * sdpMLineIndex + 1) * 1001 1783 | }]; 1784 | if (track) { 1785 | rtpSender = new RTCRtpSender(track, transports.dtlsTransport); 1786 | } 1787 | 1788 | if (mline.wantReceive) { 1789 | rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); 1790 | } 1791 | 1792 | transceivers[sdpMLineIndex] = { 1793 | iceGatherer: transports.iceGatherer, 1794 | iceTransport: transports.iceTransport, 1795 | dtlsTransport: transports.dtlsTransport, 1796 | localCapabilities: localCapabilities, 1797 | remoteCapabilities: null, 1798 | rtpSender: rtpSender, 1799 | rtpReceiver: rtpReceiver, 1800 | kind: kind, 1801 | mid: mid, 1802 | sendEncodingParameters: sendEncodingParameters, 1803 | recvEncodingParameters: null 1804 | }; 1805 | var transceiver = transceivers[sdpMLineIndex]; 1806 | sdp += SDPUtils.writeMediaSection(transceiver, 1807 | transceiver.localCapabilities, 'offer', self.localStreams[0]); 1808 | }); 1809 | 1810 | this._pendingOffer = transceivers; 1811 | var desc = new RTCSessionDescription({ 1812 | type: 'offer', 1813 | sdp: sdp 1814 | }); 1815 | if (arguments.length && typeof arguments[0] === 'function') { 1816 | window.setTimeout(arguments[0], 0, desc); 1817 | } 1818 | return Promise.resolve(desc); 1819 | }; 1820 | 1821 | window.RTCPeerConnection.prototype.createAnswer = function() { 1822 | var self = this; 1823 | 1824 | var sdp = SDPUtils.writeSessionBoilerplate(); 1825 | this.transceivers.forEach(function(transceiver) { 1826 | // Calculate intersection of capabilities. 1827 | var commonCapabilities = self._getCommonCapabilities( 1828 | transceiver.localCapabilities, 1829 | transceiver.remoteCapabilities); 1830 | 1831 | sdp += SDPUtils.writeMediaSection(transceiver, commonCapabilities, 1832 | 'answer', self.localStreams[0]); 1833 | }); 1834 | 1835 | var desc = new RTCSessionDescription({ 1836 | type: 'answer', 1837 | sdp: sdp 1838 | }); 1839 | if (arguments.length && typeof arguments[0] === 'function') { 1840 | window.setTimeout(arguments[0], 0, desc); 1841 | } 1842 | return Promise.resolve(desc); 1843 | }; 1844 | 1845 | window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) { 1846 | var mLineIndex = candidate.sdpMLineIndex; 1847 | if (candidate.sdpMid) { 1848 | for (var i = 0; i < this.transceivers.length; i++) { 1849 | if (this.transceivers[i].mid === candidate.sdpMid) { 1850 | mLineIndex = i; 1851 | break; 1852 | } 1853 | } 1854 | } 1855 | var transceiver = this.transceivers[mLineIndex]; 1856 | if (transceiver) { 1857 | var cand = Object.keys(candidate.candidate).length > 0 ? 1858 | SDPUtils.parseCandidate(candidate.candidate) : {}; 1859 | // Ignore Chrome's invalid candidates since Edge does not like them. 1860 | if (cand.protocol === 'tcp' && cand.port === 0) { 1861 | return; 1862 | } 1863 | // Ignore RTCP candidates, we assume RTCP-MUX. 1864 | if (cand.component !== '1') { 1865 | return; 1866 | } 1867 | // A dirty hack to make samples work. 1868 | if (cand.type === 'endOfCandidates') { 1869 | cand = {}; 1870 | } 1871 | transceiver.iceTransport.addRemoteCandidate(cand); 1872 | 1873 | // update the remoteDescription. 1874 | var sections = SDPUtils.splitSections(this.remoteDescription.sdp); 1875 | sections[mLineIndex + 1] += (cand.type ? candidate.candidate.trim() 1876 | : 'a=end-of-candidates') + '\r\n'; 1877 | this.remoteDescription.sdp = sections.join(''); 1878 | } 1879 | if (arguments.length > 1 && typeof arguments[1] === 'function') { 1880 | window.setTimeout(arguments[1], 0); 1881 | } 1882 | return Promise.resolve(); 1883 | }; 1884 | 1885 | window.RTCPeerConnection.prototype.getStats = function() { 1886 | var promises = []; 1887 | this.transceivers.forEach(function(transceiver) { 1888 | ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', 1889 | 'dtlsTransport'].forEach(function(method) { 1890 | if (transceiver[method]) { 1891 | promises.push(transceiver[method].getStats()); 1892 | } 1893 | }); 1894 | }); 1895 | var cb = arguments.length > 1 && typeof arguments[1] === 'function' && 1896 | arguments[1]; 1897 | return new Promise(function(resolve) { 1898 | var results = {}; 1899 | Promise.all(promises).then(function(res) { 1900 | res.forEach(function(result) { 1901 | Object.keys(result).forEach(function(id) { 1902 | results[id] = result[id]; 1903 | }); 1904 | }); 1905 | if (cb) { 1906 | window.setTimeout(cb, 0, results); 1907 | } 1908 | resolve(results); 1909 | }); 1910 | }); 1911 | }; 1912 | }, 1913 | 1914 | // Attach a media stream to an element. 1915 | attachMediaStream: function(element, stream) { 1916 | logging('DEPRECATED, attachMediaStream will soon be removed.'); 1917 | element.srcObject = stream; 1918 | }, 1919 | 1920 | reattachMediaStream: function(to, from) { 1921 | logging('DEPRECATED, reattachMediaStream will soon be removed.'); 1922 | to.srcObject = from.srcObject; 1923 | } 1924 | }; 1925 | 1926 | // Expose public methods. 1927 | module.exports = { 1928 | shimPeerConnection: edgeShim.shimPeerConnection, 1929 | attachMediaStream: edgeShim.attachMediaStream, 1930 | reattachMediaStream: edgeShim.reattachMediaStream 1931 | }; 1932 | 1933 | },{"../utils":9,"./edge_sdp":4}],6:[function(require,module,exports){ 1934 | /* 1935 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 1936 | * 1937 | * Use of this source code is governed by a BSD-style license 1938 | * that can be found in the LICENSE file in the root of the source 1939 | * tree. 1940 | */ 1941 | /* eslint-env node */ 1942 | 'use strict'; 1943 | 1944 | var logging = require('../utils').log; 1945 | var browserDetails = require('../utils').browserDetails; 1946 | 1947 | var firefoxShim = { 1948 | shimOnTrack: function() { 1949 | if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in 1950 | window.RTCPeerConnection.prototype)) { 1951 | Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { 1952 | get: function() { 1953 | return this._ontrack; 1954 | }, 1955 | set: function(f) { 1956 | if (this._ontrack) { 1957 | this.removeEventListener('track', this._ontrack); 1958 | this.removeEventListener('addstream', this._ontrackpoly); 1959 | } 1960 | this.addEventListener('track', this._ontrack = f); 1961 | this.addEventListener('addstream', this._ontrackpoly = function(e) { 1962 | e.stream.getTracks().forEach(function(track) { 1963 | var event = new Event('track'); 1964 | event.track = track; 1965 | event.receiver = {track: track}; 1966 | event.streams = [e.stream]; 1967 | this.dispatchEvent(event); 1968 | }.bind(this)); 1969 | }.bind(this)); 1970 | } 1971 | }); 1972 | } 1973 | }, 1974 | 1975 | shimSourceObject: function() { 1976 | // Firefox has supported mozSrcObject since FF22, unprefixed in 42. 1977 | if (typeof window === 'object') { 1978 | if (window.HTMLMediaElement && 1979 | !('srcObject' in window.HTMLMediaElement.prototype)) { 1980 | // Shim the srcObject property, once, when HTMLMediaElement is found. 1981 | Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { 1982 | get: function() { 1983 | return this.mozSrcObject; 1984 | }, 1985 | set: function(stream) { 1986 | this.mozSrcObject = stream; 1987 | } 1988 | }); 1989 | } 1990 | } 1991 | }, 1992 | 1993 | shimPeerConnection: function() { 1994 | // The RTCPeerConnection object. 1995 | if (!window.RTCPeerConnection) { 1996 | window.RTCPeerConnection = function(pcConfig, pcConstraints) { 1997 | if (browserDetails.version < 38) { 1998 | // .urls is not supported in FF < 38. 1999 | // create RTCIceServers with a single url. 2000 | if (pcConfig && pcConfig.iceServers) { 2001 | var newIceServers = []; 2002 | for (var i = 0; i < pcConfig.iceServers.length; i++) { 2003 | var server = pcConfig.iceServers[i]; 2004 | if (server.hasOwnProperty('urls')) { 2005 | for (var j = 0; j < server.urls.length; j++) { 2006 | var newServer = { 2007 | url: server.urls[j] 2008 | }; 2009 | if (server.urls[j].indexOf('turn') === 0) { 2010 | newServer.username = server.username; 2011 | newServer.credential = server.credential; 2012 | } 2013 | newIceServers.push(newServer); 2014 | } 2015 | } else { 2016 | newIceServers.push(pcConfig.iceServers[i]); 2017 | } 2018 | } 2019 | pcConfig.iceServers = newIceServers; 2020 | } 2021 | } 2022 | return new mozRTCPeerConnection(pcConfig, pcConstraints); 2023 | }; 2024 | window.RTCPeerConnection.prototype = mozRTCPeerConnection.prototype; 2025 | 2026 | // wrap static methods. Currently just generateCertificate. 2027 | if (mozRTCPeerConnection.generateCertificate) { 2028 | Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { 2029 | get: function() { 2030 | return mozRTCPeerConnection.generateCertificate; 2031 | } 2032 | }); 2033 | } 2034 | 2035 | window.RTCSessionDescription = mozRTCSessionDescription; 2036 | window.RTCIceCandidate = mozRTCIceCandidate; 2037 | } 2038 | 2039 | // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. 2040 | ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] 2041 | .forEach(function(method) { 2042 | var nativeMethod = RTCPeerConnection.prototype[method]; 2043 | RTCPeerConnection.prototype[method] = function() { 2044 | arguments[0] = new ((method === 'addIceCandidate')? 2045 | RTCIceCandidate : RTCSessionDescription)(arguments[0]); 2046 | return nativeMethod.apply(this, arguments); 2047 | }; 2048 | }); 2049 | }, 2050 | 2051 | shimGetUserMedia: function() { 2052 | // getUserMedia constraints shim. 2053 | var getUserMedia_ = function(constraints, onSuccess, onError) { 2054 | var constraintsToFF37_ = function(c) { 2055 | if (typeof c !== 'object' || c.require) { 2056 | return c; 2057 | } 2058 | var require = []; 2059 | Object.keys(c).forEach(function(key) { 2060 | if (key === 'require' || key === 'advanced' || 2061 | key === 'mediaSource') { 2062 | return; 2063 | } 2064 | var r = c[key] = (typeof c[key] === 'object') ? 2065 | c[key] : {ideal: c[key]}; 2066 | if (r.min !== undefined || 2067 | r.max !== undefined || r.exact !== undefined) { 2068 | require.push(key); 2069 | } 2070 | if (r.exact !== undefined) { 2071 | if (typeof r.exact === 'number') { 2072 | r. min = r.max = r.exact; 2073 | } else { 2074 | c[key] = r.exact; 2075 | } 2076 | delete r.exact; 2077 | } 2078 | if (r.ideal !== undefined) { 2079 | c.advanced = c.advanced || []; 2080 | var oc = {}; 2081 | if (typeof r.ideal === 'number') { 2082 | oc[key] = {min: r.ideal, max: r.ideal}; 2083 | } else { 2084 | oc[key] = r.ideal; 2085 | } 2086 | c.advanced.push(oc); 2087 | delete r.ideal; 2088 | if (!Object.keys(r).length) { 2089 | delete c[key]; 2090 | } 2091 | } 2092 | }); 2093 | if (require.length) { 2094 | c.require = require; 2095 | } 2096 | return c; 2097 | }; 2098 | constraints = JSON.parse(JSON.stringify(constraints)); 2099 | if (browserDetails.version < 38) { 2100 | logging('spec: ' + JSON.stringify(constraints)); 2101 | if (constraints.audio) { 2102 | constraints.audio = constraintsToFF37_(constraints.audio); 2103 | } 2104 | if (constraints.video) { 2105 | constraints.video = constraintsToFF37_(constraints.video); 2106 | } 2107 | logging('ff37: ' + JSON.stringify(constraints)); 2108 | } 2109 | return navigator.mozGetUserMedia(constraints, onSuccess, onError); 2110 | }; 2111 | 2112 | navigator.getUserMedia = getUserMedia_; 2113 | 2114 | // Returns the result of getUserMedia as a Promise. 2115 | var getUserMediaPromise_ = function(constraints) { 2116 | return new Promise(function(resolve, reject) { 2117 | navigator.getUserMedia(constraints, resolve, reject); 2118 | }); 2119 | }; 2120 | 2121 | // Shim for mediaDevices on older versions. 2122 | if (!navigator.mediaDevices) { 2123 | navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, 2124 | addEventListener: function() { }, 2125 | removeEventListener: function() { } 2126 | }; 2127 | } 2128 | navigator.mediaDevices.enumerateDevices = 2129 | navigator.mediaDevices.enumerateDevices || function() { 2130 | return new Promise(function(resolve) { 2131 | var infos = [ 2132 | {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, 2133 | {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} 2134 | ]; 2135 | resolve(infos); 2136 | }); 2137 | }; 2138 | 2139 | if (browserDetails.version < 41) { 2140 | // Work around http://bugzil.la/1169665 2141 | var orgEnumerateDevices = 2142 | navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); 2143 | navigator.mediaDevices.enumerateDevices = function() { 2144 | return orgEnumerateDevices().then(undefined, function(e) { 2145 | if (e.name === 'NotFoundError') { 2146 | return []; 2147 | } 2148 | throw e; 2149 | }); 2150 | }; 2151 | } 2152 | }, 2153 | 2154 | // Attach a media stream to an element. 2155 | attachMediaStream: function(element, stream) { 2156 | logging('DEPRECATED, attachMediaStream will soon be removed.'); 2157 | element.srcObject = stream; 2158 | }, 2159 | 2160 | reattachMediaStream: function(to, from) { 2161 | logging('DEPRECATED, reattachMediaStream will soon be removed.'); 2162 | to.srcObject = from.srcObject; 2163 | } 2164 | }; 2165 | 2166 | // Expose public methods. 2167 | module.exports = { 2168 | shimOnTrack: firefoxShim.shimOnTrack, 2169 | shimSourceObject: firefoxShim.shimSourceObject, 2170 | shimPeerConnection: firefoxShim.shimPeerConnection, 2171 | shimGetUserMedia: require('./getusermedia'), 2172 | attachMediaStream: firefoxShim.attachMediaStream, 2173 | reattachMediaStream: firefoxShim.reattachMediaStream 2174 | }; 2175 | 2176 | },{"../utils":9,"./getusermedia":7}],7:[function(require,module,exports){ 2177 | /* 2178 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 2179 | * 2180 | * Use of this source code is governed by a BSD-style license 2181 | * that can be found in the LICENSE file in the root of the source 2182 | * tree. 2183 | */ 2184 | /* eslint-env node */ 2185 | 'use strict'; 2186 | 2187 | var logging = require('../utils').log; 2188 | var browserDetails = require('../utils').browserDetails; 2189 | 2190 | // Expose public methods. 2191 | module.exports = function() { 2192 | // getUserMedia constraints shim. 2193 | var getUserMedia_ = function(constraints, onSuccess, onError) { 2194 | var constraintsToFF37_ = function(c) { 2195 | if (typeof c !== 'object' || c.require) { 2196 | return c; 2197 | } 2198 | var require = []; 2199 | Object.keys(c).forEach(function(key) { 2200 | if (key === 'require' || key === 'advanced' || key === 'mediaSource') { 2201 | return; 2202 | } 2203 | var r = c[key] = (typeof c[key] === 'object') ? 2204 | c[key] : {ideal: c[key]}; 2205 | if (r.min !== undefined || 2206 | r.max !== undefined || r.exact !== undefined) { 2207 | require.push(key); 2208 | } 2209 | if (r.exact !== undefined) { 2210 | if (typeof r.exact === 'number') { 2211 | r. min = r.max = r.exact; 2212 | } else { 2213 | c[key] = r.exact; 2214 | } 2215 | delete r.exact; 2216 | } 2217 | if (r.ideal !== undefined) { 2218 | c.advanced = c.advanced || []; 2219 | var oc = {}; 2220 | if (typeof r.ideal === 'number') { 2221 | oc[key] = {min: r.ideal, max: r.ideal}; 2222 | } else { 2223 | oc[key] = r.ideal; 2224 | } 2225 | c.advanced.push(oc); 2226 | delete r.ideal; 2227 | if (!Object.keys(r).length) { 2228 | delete c[key]; 2229 | } 2230 | } 2231 | }); 2232 | if (require.length) { 2233 | c.require = require; 2234 | } 2235 | return c; 2236 | }; 2237 | constraints = JSON.parse(JSON.stringify(constraints)); 2238 | if (browserDetails.version < 38) { 2239 | logging('spec: ' + JSON.stringify(constraints)); 2240 | if (constraints.audio) { 2241 | constraints.audio = constraintsToFF37_(constraints.audio); 2242 | } 2243 | if (constraints.video) { 2244 | constraints.video = constraintsToFF37_(constraints.video); 2245 | } 2246 | logging('ff37: ' + JSON.stringify(constraints)); 2247 | } 2248 | return navigator.mozGetUserMedia(constraints, onSuccess, onError); 2249 | }; 2250 | 2251 | navigator.getUserMedia = getUserMedia_; 2252 | 2253 | // Returns the result of getUserMedia as a Promise. 2254 | var getUserMediaPromise_ = function(constraints) { 2255 | return new Promise(function(resolve, reject) { 2256 | navigator.getUserMedia(constraints, resolve, reject); 2257 | }); 2258 | }; 2259 | 2260 | // Shim for mediaDevices on older versions. 2261 | if (!navigator.mediaDevices) { 2262 | navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, 2263 | addEventListener: function() { }, 2264 | removeEventListener: function() { } 2265 | }; 2266 | } 2267 | navigator.mediaDevices.enumerateDevices = 2268 | navigator.mediaDevices.enumerateDevices || function() { 2269 | return new Promise(function(resolve) { 2270 | var infos = [ 2271 | {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, 2272 | {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} 2273 | ]; 2274 | resolve(infos); 2275 | }); 2276 | }; 2277 | 2278 | if (browserDetails.version < 41) { 2279 | // Work around http://bugzil.la/1169665 2280 | var orgEnumerateDevices = 2281 | navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); 2282 | navigator.mediaDevices.enumerateDevices = function() { 2283 | return orgEnumerateDevices().then(undefined, function(e) { 2284 | if (e.name === 'NotFoundError') { 2285 | return []; 2286 | } 2287 | throw e; 2288 | }); 2289 | }; 2290 | } 2291 | }; 2292 | 2293 | },{"../utils":9}],8:[function(require,module,exports){ 2294 | /* 2295 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 2296 | * 2297 | * Use of this source code is governed by a BSD-style license 2298 | * that can be found in the LICENSE file in the root of the source 2299 | * tree. 2300 | */ 2301 | 'use strict'; 2302 | var safariShim = { 2303 | // TODO: DrAlex, should be here, double check against LayoutTests 2304 | // shimOnTrack: function() { }, 2305 | 2306 | // TODO: DrAlex 2307 | // attachMediaStream: function(element, stream) { }, 2308 | // reattachMediaStream: function(to, from) { }, 2309 | 2310 | // TODO: once the back-end for the mac port is done, add. 2311 | // TODO: check for webkitGTK+ 2312 | // shimPeerConnection: function() { }, 2313 | 2314 | shimGetUserMedia: function() { 2315 | navigator.getUserMedia = navigator.webkitGetUserMedia; 2316 | } 2317 | }; 2318 | 2319 | // Expose public methods. 2320 | module.exports = { 2321 | shimGetUserMedia: safariShim.shimGetUserMedia 2322 | // TODO 2323 | // shimOnTrack: safariShim.shimOnTrack, 2324 | // shimPeerConnection: safariShim.shimPeerConnection, 2325 | // attachMediaStream: safariShim.attachMediaStream, 2326 | // reattachMediaStream: safariShim.reattachMediaStream 2327 | }; 2328 | 2329 | },{}],9:[function(require,module,exports){ 2330 | /* 2331 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 2332 | * 2333 | * Use of this source code is governed by a BSD-style license 2334 | * that can be found in the LICENSE file in the root of the source 2335 | * tree. 2336 | */ 2337 | /* eslint-env node */ 2338 | 'use strict'; 2339 | 2340 | var logDisabled_ = false; 2341 | 2342 | // Utility methods. 2343 | var utils = { 2344 | disableLog: function(bool) { 2345 | if (typeof bool !== 'boolean') { 2346 | return new Error('Argument type: ' + typeof bool + 2347 | '. Please use a boolean.'); 2348 | } 2349 | logDisabled_ = bool; 2350 | return (bool) ? 'adapter.js logging disabled' : 2351 | 'adapter.js logging enabled'; 2352 | }, 2353 | 2354 | log: function() { 2355 | if (typeof window === 'object') { 2356 | if (logDisabled_) { 2357 | return; 2358 | } 2359 | if (typeof console !== 'undefined' && typeof console.log === 'function') { 2360 | console.log.apply(console, arguments); 2361 | } 2362 | } 2363 | }, 2364 | 2365 | /** 2366 | * Extract browser version out of the provided user agent string. 2367 | * 2368 | * @param {!string} uastring userAgent string. 2369 | * @param {!string} expr Regular expression used as match criteria. 2370 | * @param {!number} pos position in the version string to be returned. 2371 | * @return {!number} browser version. 2372 | */ 2373 | extractVersion: function(uastring, expr, pos) { 2374 | var match = uastring.match(expr); 2375 | return match && match.length >= pos && parseInt(match[pos], 10); 2376 | }, 2377 | 2378 | /** 2379 | * Browser detector. 2380 | * 2381 | * @return {object} result containing browser, version and minVersion 2382 | * properties. 2383 | */ 2384 | detectBrowser: function() { 2385 | // Returned result object. 2386 | var result = {}; 2387 | result.browser = null; 2388 | result.version = null; 2389 | result.minVersion = null; 2390 | 2391 | // Fail early if it's not a browser 2392 | if (typeof window === 'undefined' || !window.navigator) { 2393 | result.browser = 'Not a browser.'; 2394 | return result; 2395 | } 2396 | 2397 | // Firefox. 2398 | if (navigator.mozGetUserMedia) { 2399 | result.browser = 'firefox'; 2400 | result.version = this.extractVersion(navigator.userAgent, 2401 | /Firefox\/([0-9]+)\./, 1); 2402 | result.minVersion = 31; 2403 | 2404 | // all webkit-based browsers 2405 | } else if (navigator.webkitGetUserMedia) { 2406 | // Chrome, Chromium, Webview, Opera, all use the chrome shim for now 2407 | if (window.webkitRTCPeerConnection) { 2408 | result.browser = 'chrome'; 2409 | result.version = this.extractVersion(navigator.userAgent, 2410 | /Chrom(e|ium)\/([0-9]+)\./, 2); 2411 | result.minVersion = 38; 2412 | 2413 | // Safari or unknown webkit-based 2414 | // for the time being Safari has support for MediaStreams but not webRTC 2415 | } else { 2416 | // Safari UA substrings of interest for reference: 2417 | // - webkit version: AppleWebKit/602.1.25 (also used in Op,Cr) 2418 | // - safari UI version: Version/9.0.3 (unique to Safari) 2419 | // - safari UI webkit version: Safari/601.4.4 (also used in Op,Cr) 2420 | // 2421 | // if the webkit version and safari UI webkit versions are equals, 2422 | // ... this is a stable version. 2423 | // 2424 | // only the internal webkit version is important today to know if 2425 | // media streams are supported 2426 | // 2427 | if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) { 2428 | result.browser = 'safari'; 2429 | result.version = this.extractVersion(navigator.userAgent, 2430 | /AppleWebKit\/([0-9]+)\./, 1); 2431 | result.minVersion = 602; 2432 | 2433 | // unknown webkit-based browser 2434 | } else { 2435 | result.browser = 'Unsupported webkit-based browser ' + 2436 | 'with GUM support but no WebRTC support.'; 2437 | return result; 2438 | } 2439 | } 2440 | 2441 | // Edge. 2442 | } else if (navigator.mediaDevices && 2443 | navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { 2444 | result.browser = 'edge'; 2445 | result.version = this.extractVersion(navigator.userAgent, 2446 | /Edge\/(\d+).(\d+)$/, 2); 2447 | result.minVersion = 10547; 2448 | 2449 | // Default fallthrough: not supported. 2450 | } else { 2451 | result.browser = 'Not a supported browser.'; 2452 | return result; 2453 | } 2454 | 2455 | // Warn if version is less than minVersion. 2456 | if (result.version < result.minVersion) { 2457 | utils.log('Browser: ' + result.browser + ' Version: ' + result.version + 2458 | ' < minimum supported version: ' + result.minVersion + 2459 | '\n some things might not work!'); 2460 | } 2461 | 2462 | return result; 2463 | } 2464 | }; 2465 | 2466 | // Export. 2467 | module.exports = { 2468 | log: utils.log, 2469 | disableLog: utils.disableLog, 2470 | browserDetails: utils.detectBrowser(), 2471 | extractVersion: utils.extractVersion 2472 | }; 2473 | 2474 | },{}]},{},[1])(1) 2475 | }); -------------------------------------------------------------------------------- /webRTCserver/static/js/lib/socket.io.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t,e){"object"===("undefined"==typeof t?"undefined":o(t))&&(e=t,t=void 0),e=e||{};var n,r=i(t),s=r.source,u=r.id,h=r.path,f=p[u]&&h in p[u].nsps,l=e.forceNew||e["force new connection"]||!1===e.multiplex||f;return l?(c("ignoring socket cache for %s",s),n=a(s,e)):(p[u]||(c("new io instance for %s",s),p[u]=a(s,e)),n=p[u]),r.query&&!e.query&&(e.query=r.query),n.socket(r.path,e)}var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=n(1),s=n(7),a=n(13),c=n(3)("socket.io-client");t.exports=e=r;var p=e.managers={};e.protocol=s.protocol,e.connect=r,e.Manager=n(13),e.Socket=n(39)},function(t,e,n){(function(e){"use strict";function r(t,n){var r=t;n=n||e.location,null==t&&(t=n.protocol+"//"+n.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?n.protocol+t:n.host+t),/^(https?|wss?):\/\//.test(t)||(i("protocol-less url %s",t),t="undefined"!=typeof n?n.protocol+"//"+t:"https://"+t),i("parse %s",t),r=o(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var s=r.host.indexOf(":")!==-1,a=s?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+a+":"+r.port,r.href=r.protocol+"://"+a+(n&&n.port===r.port?"":":"+r.port),r}var o=n(2),i=n(3)("socket.io-client:url");t.exports=r}).call(e,function(){return this}())},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");o!=-1&&i!=-1&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s=n.exec(t||""),a={},c=14;c--;)a[r[c]]=s[c]||"";return o!=-1&&i!=-1&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a}},function(t,e,n){(function(r){function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(t){var n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+e.humanize(this.diff),n){var r="color: "+this.color;t.splice(1,0,r,"color: inherit");var o=0,i=0;t[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(o++,"%c"===t&&(i=o))}),t.splice(i,0,r)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{null==t?e.storage.removeItem("debug"):e.storage.debug=t}catch(n){}}function c(){var t;try{t=e.storage.debug}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function p(){try{return window.localStorage}catch(t){}}e=t.exports=n(5),e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:p(),e.colors=["lightseagreen","forestgreen","goldenrod","dodgerblue","darkorchid","crimson"],e.formatters.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},e.enable(c())}).call(e,n(4))},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(u===setTimeout)return setTimeout(t,0);if((u===n||!u)&&setTimeout)return u=setTimeout,setTimeout(t,0);try{return u(t,0)}catch(e){try{return u.call(null,t,0)}catch(e){return u.call(this,t,0)}}}function i(t){if(h===clearTimeout)return clearTimeout(t);if((h===r||!h)&&clearTimeout)return h=clearTimeout,clearTimeout(t);try{return h(t)}catch(e){try{return h.call(null,t)}catch(e){return h.call(this,t)}}}function s(){y&&l&&(y=!1,l.length?d=l.concat(d):m=-1,d.length&&a())}function a(){if(!y){var t=o(s);y=!0;for(var e=d.length;e;){for(l=d,d=[];++m1)for(var n=1;n100)){var e=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*u;case"days":case"day":case"d":return n*p;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){return t>=p?Math.round(t/p)+"d":t>=c?Math.round(t/c)+"h":t>=a?Math.round(t/a)+"m":t>=s?Math.round(t/s)+"s":t+"ms"}function o(t){return i(t,p,"day")||i(t,c,"hour")||i(t,a,"minute")||i(t,s,"second")||t+" ms"}function i(t,e,n){if(!(t0)return n(t);if("number"===i&&isNaN(t)===!1)return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(){}function o(t){var n=""+t.type;return e.BINARY_EVENT!==t.type&&e.BINARY_ACK!==t.type||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data&&(n+=JSON.stringify(t.data)),h("encoded %j as %s",t,n),n}function i(t,e){function n(t){var n=d.deconstructPacket(t),r=o(n.packet),i=n.buffers;i.unshift(r),e(i)}d.removeBlobs(t,n)}function s(){this.reconstructor=null}function a(t){var n=0,r={type:Number(t.charAt(0))};if(null==e.types[r.type])return u();if(e.BINARY_EVENT===r.type||e.BINARY_ACK===r.type){for(var o="";"-"!==t.charAt(++n)&&(o+=t.charAt(n),n!=t.length););if(o!=Number(o)||"-"!==t.charAt(n))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(n+1))for(r.nsp="";++n;){var i=t.charAt(n);if(","===i)break;if(r.nsp+=i,n===t.length)break}else r.nsp="/";var s=t.charAt(n+1);if(""!==s&&Number(s)==s){for(r.id="";++n;){var i=t.charAt(n);if(null==i||Number(i)!=i){--n;break}if(r.id+=t.charAt(n),n===t.length)break}r.id=Number(r.id)}return t.charAt(++n)&&(r=c(r,t.substr(n))),h("decoded %s as %j",t,r),r}function c(t,e){try{t.data=JSON.parse(e)}catch(n){return u()}return t}function p(t){this.reconPack=t,this.buffers=[]}function u(){return{type:e.ERROR,data:"parser error"}}var h=n(3)("socket.io-parser"),f=n(8),l=n(9),d=n(11),y=n(12);e.protocol=4,e.types=["CONNECT","DISCONNECT","EVENT","ACK","ERROR","BINARY_EVENT","BINARY_ACK"],e.CONNECT=0,e.DISCONNECT=1,e.EVENT=2,e.ACK=3,e.ERROR=4,e.BINARY_EVENT=5,e.BINARY_ACK=6,e.Encoder=r,e.Decoder=s,r.prototype.encode=function(t,n){if(t.type!==e.EVENT&&t.type!==e.ACK||!l(t.data)||(t.type=t.type===e.EVENT?e.BINARY_EVENT:e.BINARY_ACK),h("encoding packet %j",t),e.BINARY_EVENT===t.type||e.BINARY_ACK===t.type)i(t,n);else{var r=o(t);n([r])}},f(s.prototype),s.prototype.add=function(t){var n;if("string"==typeof t)n=a(t),e.BINARY_EVENT===n.type||e.BINARY_ACK===n.type?(this.reconstructor=new p(n),0===this.reconstructor.reconPack.attachments&&this.emit("decoded",n)):this.emit("decoded",n);else{if(!y(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");n=this.reconstructor.takeBinaryData(t),n&&(this.reconstructor=null,this.emit("decoded",n))}},s.prototype.destroy=function(){this.reconstructor&&this.reconstructor.finishedReconstruction()},p.prototype.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=d.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null},p.prototype.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]}},function(t,e,n){function r(t){if(t)return o(t)}function o(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,o=0;o0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},r.prototype.cleanup=function(){h("cleanup");for(var t=this.subs.length,e=0;e=this._reconnectionAttempts)h("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();h("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(h("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(h("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(h("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(15)},function(t,e,n){t.exports=n(16),t.exports.parser=n(23)},function(t,e,n){(function(e){function r(t,n){if(!(this instanceof r))return new r(t,n);n=n||{},t&&"object"==typeof t&&(n=t,t=null),t?(t=u(t),n.hostname=t.host,n.secure="https"===t.protocol||"wss"===t.protocol,n.port=t.port,t.query&&(n.query=t.query)):n.host&&(n.hostname=u(n.host).host),this.secure=null!=n.secure?n.secure:e.location&&"https:"===location.protocol,n.hostname&&!n.port&&(n.port=this.secure?"443":"80"),this.agent=n.agent||!1,this.hostname=n.hostname||(e.location?location.hostname:"localhost"),this.port=n.port||(e.location&&location.port?location.port:this.secure?443:80),this.query=n.query||{},"string"==typeof this.query&&(this.query=f.decode(this.query)),this.upgrade=!1!==n.upgrade,this.path=(n.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!n.forceJSONP,this.jsonp=!1!==n.jsonp,this.forceBase64=!!n.forceBase64,this.enablesXDR=!!n.enablesXDR,this.timestampParam=n.timestampParam||"t",this.timestampRequests=n.timestampRequests,this.transports=n.transports||["polling","websocket"],this.transportOptions=n.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=n.policyPort||843,this.rememberUpgrade=n.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=n.onlyBinaryUpgrades,this.perMessageDeflate=!1!==n.perMessageDeflate&&(n.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=n.pfx||null,this.key=n.key||null,this.passphrase=n.passphrase||null,this.cert=n.cert||null,this.ca=n.ca||null,this.ciphers=n.ciphers||null,this.rejectUnauthorized=void 0===n.rejectUnauthorized||n.rejectUnauthorized,this.forceNode=!!n.forceNode;var o="object"==typeof e&&e;o.global===o&&(n.extraHeaders&&Object.keys(n.extraHeaders).length>0&&(this.extraHeaders=n.extraHeaders),n.localAddress&&(this.localAddress=n.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,this.open()}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(17),s=n(8),a=n(3)("engine.io-client:socket"),c=n(37),p=n(23),u=n(2),h=n(38),f=n(31);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=p.protocol,r.Socket=r,r.Transport=n(22),r.transports=n(17),r.parser=n(23),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=p.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),u.send([{type:"ping",data:"probe"}]),u.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",u),!u)return;r.priorWebsocketSuccess="websocket"===u.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),p(),f.setTransport(u),u.send([{type:"upgrade"}]),f.emit("upgrade",u),u=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=u.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,p(),u.close(),u=null)}function o(e){var r=new Error("probe error: "+e);r.transport=u.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){u&&t.name!==u.name&&(a('"%s" works - aborting "%s"',t.name,u.name),n())}function p(){u.removeListener("open",e),u.removeListener("error",o),u.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var u=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,u.once("open",e),u.once("error",o),u.once("close",i),this.once("close",s),this.once("upgrading",c),u.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:w}var i=new Uint8Array(t),o=i[0],s=f(t,1);return k&&"blob"===n&&(s=new k([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!p)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=p.decode(t.substr(1));return"blob"===e&&k&&(r=new k([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?k&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(w,0,1);for(var i,s,a="",c=0,p=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",p=1;255!==s[p];p++){if(c.length>310)return r(w,0,1);c+=s[p]}o=f(o,2+c.length),c=parseInt(c);var u=f(o,0,c);if(a)try{u=String.fromCharCode.apply(null,new Uint8Array(u))}catch(h){var l=new Uint8Array(u);u="";for(var p=0;pr&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=w(e>>>10&1023|55296),e=56320|1023&e),o+=w(e);return o}function c(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function p(t,e){return w(t>>e&63|128)}function u(t,e){if(0==(4294967168&t))return w(t);var n="";return 0==(4294965248&t)?n=w(t>>6&31|192):0==(4294901760&t)?(c(t,e)||(t=65533),n=w(t>>12&15|224),n+=p(t,6)):0==(4292870144&t)&&(n=w(t>>18&7|240),n+=p(t,12),n+=p(t,6)),n+=w(63&t|128)}function h(t,e){e=e||{};for(var n,r=!1!==e.strict,o=s(t),i=o.length,a=-1,c="";++a=v)throw Error("Invalid byte index");var t=255&g[b];if(b++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function l(t){var e,n,r,o,i;if(b>v)throw Error("Invalid byte index");if(b==v)return!1;if(e=255&g[b],b++,0==(128&e))return e;if(192==(224&e)){if(n=f(),i=(31&e)<<6|n,i>=128)return i;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=f(),r=f(),i=(15&e)<<12|n<<6|r,i>=2048)return c(i,t)?i:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=f(),r=f(),o=f(),i=(7&e)<<18|n<<12|r<<6|o,i>=65536&&i<=1114111))return i;throw Error("Invalid UTF-8 detected")}function d(t,e){e=e||{};var n=!1!==e.strict;g=s(t),v=g.length,b=0;for(var r,o=[];(r=l(n))!==!1;)o.push(r);return a(o)}var y="object"==typeof e&&e,m=("object"==typeof t&&t&&t.exports==y&&t,"object"==typeof o&&o);m.global!==m&&m.window!==m||(i=m);var g,v,b,w=String.fromCharCode,k={version:"2.1.2",encode:h,decode:d};r=function(){return k}.call(e,n,e,t),!(void 0!==r&&(t.exports=r))}(this)}).call(e,n(28)(t),function(){return this}())},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,p=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var u=new ArrayBuffer(a),h=new Uint8Array(u);for(e=0;e>4,h[p++]=(15&o)<<4|i>>2,h[p++]=(3&i)<<6|63&s;return u}}()},function(t,e){(function(e){function n(t){for(var e=0;e0);return e}function r(t){var e=0;for(u=0;u';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),u=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=u,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(p,"\\\n"),this.area.value=t.replace(c,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){(function(e){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=h&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=o),i.call(this,t)}var o,i=n(22),s=n(23),a=n(31),c=n(32),p=n(33),u=n(3)("engine.io-client:websocket"),h=e.WebSocket||e.MozWebSocket;if("undefined"==typeof window)try{o=n(36)}catch(f){}var l=h;l||"undefined"!=typeof window||(l=o),t.exports=r,c(r,i),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function n(){r.emit("flush"),setTimeout(function(){r.writable=!0,r.emit("drain")},0)}var r=this;this.writable=!1;for(var o=t.length,i=0,a=o;i0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])}); 3 | //# sourceMappingURL=socket.io.js.map -------------------------------------------------------------------------------- /webRTCserver/static/js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isChannelReady = false; 4 | var isInitiator = false; 5 | var isStarted = false; 6 | var localStream; 7 | var pc; 8 | var remoteStream; 9 | var turnReady; 10 | var receiveChannel; 11 | var sendChannel; 12 | var sendText = document.querySelector("#text"); 13 | var chatlog = document.querySelector('#chatlog'); 14 | 15 | var pcConfig = { 16 | 'iceServers': [{ 17 | 'urls': 'stun:stun.l.google.com:19302' 18 | }] 19 | }; 20 | 21 | // Set up audio and video regardless of what devices are present. 22 | var sdpConstraints = { 23 | offerToReceiveAudio: true, 24 | offerToReceiveVideo: true 25 | }; 26 | 27 | ///////////////////////////////////////////// 28 | 29 | //room = 'foo'; 30 | // Could prompt for room name: 31 | // var room = prompt('Enter room name:'); 32 | 33 | var socket = io.connect(); 34 | 35 | if (room !== '') { 36 | socket.emit('create or join', room); 37 | console.log('Attempted to create or join room', room); 38 | } 39 | 40 | socket.on('created', function(room) { 41 | console.log('Created room ' + room); 42 | isInitiator = true; 43 | }); 44 | 45 | socket.on('full', function(room) { 46 | console.log('Room ' + room + ' is full'); 47 | }); 48 | 49 | socket.on('join', function (room){ 50 | console.log('Another peer made a request to join room ' + room); 51 | console.log('This peer is the initiator of room ' + room + '!'); 52 | isChannelReady = true; 53 | }); 54 | 55 | socket.on('joined', function(room) { 56 | console.log('joined: ' + room); 57 | isChannelReady = true; 58 | }); 59 | 60 | socket.on('log', function(array) { 61 | console.log.apply(console, array); 62 | }); 63 | 64 | //////////////////////////////////////////////// 65 | 66 | function sendMessage(message) { 67 | console.log('Client sending message: ', message); 68 | socket.emit('message', message); 69 | } 70 | 71 | // This client receives a message 72 | socket.on('message', function(message) { 73 | console.log('Client received message:', message); 74 | if (message === 'got user media') { 75 | maybeStart(); 76 | } else if (message.type === 'offer') { 77 | if (!isInitiator && !isStarted) { 78 | maybeStart(); 79 | } 80 | pc.setRemoteDescription(new RTCSessionDescription(message)); 81 | doAnswer(); 82 | } else if (message.type === 'answer' && isStarted) { 83 | pc.setRemoteDescription(new RTCSessionDescription(message)); 84 | } else if (message.type === 'candidate' && isStarted) { 85 | var candidate = new RTCIceCandidate({ 86 | sdpMLineIndex: message.label, 87 | candidate: message.candidate 88 | }); 89 | pc.addIceCandidate(candidate); 90 | } else if (message === 'bye' && isStarted) { 91 | handleRemoteHangup(); 92 | } 93 | }); 94 | 95 | //////////////////////////////////////////////////// 96 | 97 | var localVideo = document.querySelector('#localVideo'); 98 | var remoteVideo = document.querySelector('#remoteVideo'); 99 | 100 | navigator.mediaDevices.getUserMedia({ 101 | audio: false, 102 | video: true 103 | }) 104 | .then(gotStream) 105 | .catch(function(e) { 106 | alert('getUserMedia() error: ' + e.name); 107 | }); 108 | 109 | function gotStream(stream) { 110 | console.log('Adding local stream.'); 111 | localVideo.src = window.URL.createObjectURL(stream); 112 | localStream = stream; 113 | sendMessage('got user media'); 114 | if (isInitiator) { 115 | maybeStart(); 116 | } 117 | } 118 | 119 | var constraints = { 120 | video: true 121 | }; 122 | 123 | console.log('Getting user media with constraints', constraints); 124 | 125 | if (false) { 126 | requestTurn( 127 | 'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913' 128 | ); 129 | } 130 | 131 | function maybeStart() { 132 | console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady); 133 | if (!isStarted && typeof localStream !== 'undefined' && isChannelReady) { 134 | console.log('>>>>>> creating peer connection'); 135 | createPeerConnection(); 136 | pc.addStream(localStream); 137 | isStarted = true; 138 | console.log('isInitiator', isInitiator); 139 | if (isInitiator) { 140 | doCall(); 141 | } 142 | } 143 | } 144 | 145 | window.onbeforeunload = function() { 146 | socket.emit('disconnect', room) 147 | console.log("sending disconnect") 148 | }; 149 | 150 | ///////////////////////////////////////////////////////// 151 | 152 | function createPeerConnection() { 153 | try { 154 | pc = new RTCPeerConnection(pcConfig); 155 | sendChannel = pc.createDataChannel('chat', null); 156 | pc.onicecandidate = handleIceCandidate; 157 | pc.onaddstream = handleRemoteStreamAdded; 158 | pc.onremovestream = handleRemoteStreamRemoved; 159 | pc.ondatachannel = handleChannelCallback; 160 | 161 | console.log('Created RTCPeerConnnection'); 162 | } catch (e) { 163 | console.log('Failed to create PeerConnection, exception: ' + e.message); 164 | alert('Cannot create RTCPeerConnection object.'); 165 | return; 166 | } 167 | } 168 | 169 | function handleChannelCallback(event) { 170 | receiveChannel = event.channel; 171 | receiveChannel.onmessage = onReceiveMessageCallback; 172 | } 173 | 174 | function onReceiveMessageCallback(event) { 175 | var text = document.createElement("P"); 176 | text.appendChild(document.createTextNode(event.data)) 177 | 178 | chatlog.appendChild(text); 179 | } 180 | 181 | function handleIceCandidate(event) { 182 | console.log('icecandidate event: ', event); 183 | if (event.candidate) { 184 | sendMessage({ 185 | type: 'candidate', 186 | label: event.candidate.sdpMLineIndex, 187 | id: event.candidate.sdpMid, 188 | candidate: event.candidate.candidate 189 | }); 190 | } else { 191 | console.log('End of candidates.'); 192 | } 193 | } 194 | 195 | function sendData() { 196 | var text = document.createElement("P"); 197 | text.appendChild(document.createTextNode(sendText.value)); 198 | chatlog.appendChild(text); 199 | sendChannel.send(sendText.value); 200 | sendText.value = ''; 201 | } 202 | 203 | function handleRemoteStreamAdded(event) { 204 | console.log('Remote stream added.'); 205 | remoteVideo.src = window.URL.createObjectURL(event.stream); 206 | remoteStream = event.stream; 207 | } 208 | 209 | function handleCreateOfferError(event) { 210 | console.log('createOffer() error: ', event); 211 | } 212 | 213 | function doCall() { 214 | console.log('Sending offer to peer'); 215 | pc.createOffer(setLocalAndSendMessage, handleCreateOfferError); 216 | } 217 | 218 | function doAnswer() { 219 | console.log('Sending answer to peer.'); 220 | pc.createAnswer().then( 221 | setLocalAndSendMessage, 222 | onCreateSessionDescriptionError 223 | ); 224 | } 225 | 226 | function setLocalAndSendMessage(sessionDescription) { 227 | // Set Opus as the preferred codec in SDP if Opus is present. 228 | // sessionDescription.sdp = preferOpus(sessionDescription.sdp); 229 | pc.setLocalDescription(sessionDescription); 230 | console.log('setLocalAndSendMessage sending message', sessionDescription); 231 | sendMessage(sessionDescription); 232 | } 233 | 234 | function onCreateSessionDescriptionError(error) { 235 | //trace('Failed to create session description: ' + error.toString()); 236 | } 237 | 238 | function requestTurn(turnURL) { 239 | var turnExists = false; 240 | for (var i in pcConfig.iceServers) { 241 | if (pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') { 242 | turnExists = true; 243 | turnReady = true; 244 | break; 245 | } 246 | } 247 | if (!turnExists) { 248 | console.log('Getting TURN server from ', turnURL); 249 | // No TURN server. Get one from computeengineondemand.appspot.com: 250 | var xhr = new XMLHttpRequest(); 251 | xhr.onreadystatechange = function() { 252 | if (xhr.readyState === 4 && xhr.status === 200) { 253 | var turnServer = JSON.parse(xhr.responseText); 254 | console.log('Got TURN server: ', turnServer); 255 | pcConfig.iceServers.push({ 256 | 'url': 'turn:' + turnServer.username + '@' + turnServer.turn, 257 | 'credential': turnServer.password 258 | }); 259 | turnReady = true; 260 | } 261 | }; 262 | xhr.open('GET', turnURL, true); 263 | xhr.send(); 264 | } 265 | } 266 | 267 | function handleRemoteStreamAdded(event) { 268 | console.log('Remote stream added.'); 269 | remoteVideo.src = window.URL.createObjectURL(event.stream); 270 | remoteStream = event.stream; 271 | } 272 | 273 | function handleRemoteStreamRemoved(event) { 274 | console.log('Remote stream removed. Event: ', event); 275 | } 276 | 277 | function hangup() { 278 | console.log('Hanging up.'); 279 | stop(); 280 | sendMessage('bye'); 281 | } 282 | 283 | function handleRemoteHangup() { 284 | console.log('Session terminated.'); 285 | stop(); 286 | isInitiator = false; 287 | } 288 | 289 | function stop() { 290 | isStarted = false; 291 | // isAudioMuted = false; 292 | // isVideoMuted = false; 293 | pc.close(); 294 | pc = null; 295 | } 296 | 297 | /////////////////////////////////////////// 298 | 299 | // Set Opus as the default audio codec if it's present. 300 | function preferOpus(sdp) { 301 | var sdpLines = sdp.split('\r\n'); 302 | var mLineIndex; 303 | // Search for m line. 304 | for (var i = 0; i < sdpLines.length; i++) { 305 | if (sdpLines[i].search('m=audio') !== -1) { 306 | mLineIndex = i; 307 | break; 308 | } 309 | } 310 | if (mLineIndex === null) { 311 | return sdp; 312 | } 313 | 314 | // If Opus is available, set it as the default in m line. 315 | for (i = 0; i < sdpLines.length; i++) { 316 | if (sdpLines[i].search('opus/48000') !== -1) { 317 | var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); 318 | if (opusPayload) { 319 | sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], 320 | opusPayload); 321 | } 322 | break; 323 | } 324 | } 325 | 326 | // Remove CN in m line and sdp. 327 | sdpLines = removeCN(sdpLines, mLineIndex); 328 | 329 | sdp = sdpLines.join('\r\n'); 330 | return sdp; 331 | } 332 | 333 | function extractSdp(sdpLine, pattern) { 334 | var result = sdpLine.match(pattern); 335 | return result && result.length === 2 ? result[1] : null; 336 | } 337 | 338 | // Set the selected codec to the first in m line. 339 | function setDefaultCodec(mLine, payload) { 340 | var elements = mLine.split(' '); 341 | var newLine = []; 342 | var index = 0; 343 | for (var i = 0; i < elements.length; i++) { 344 | if (index === 3) { // Format of media starts from the fourth. 345 | newLine[index++] = payload; // Put target payload to the first. 346 | } 347 | if (elements[i] !== payload) { 348 | newLine[index++] = elements[i]; 349 | } 350 | } 351 | return newLine.join(' '); 352 | } 353 | 354 | // Strip CN from sdp before CN constraints is ready. 355 | function removeCN(sdpLines, mLineIndex) { 356 | var mLineElements = sdpLines[mLineIndex].split(' '); 357 | // Scan from end for the convenience of removing an item. 358 | for (var i = sdpLines.length - 1; i >= 0; i--) { 359 | var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); 360 | if (payload) { 361 | var cnPos = mLineElements.indexOf(payload); 362 | if (cnPos !== -1) { 363 | // Remove CN payload from m line. 364 | mLineElements.splice(cnPos, 1); 365 | } 366 | // Remove CN line in sdp 367 | sdpLines.splice(i, 1); 368 | } 369 | } 370 | 371 | sdpLines[mLineIndex] = mLineElements.join(' '); 372 | return sdpLines; 373 | } 374 | -------------------------------------------------------------------------------- /webRTCserver/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Realtime communication with WebRTC 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |

Realtime communication with WebRTC

18 | 19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /webRTCserver/webRTCserver.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, url_for 2 | import socketio 3 | import eventlet 4 | import eventlet.wsgi 5 | import datetime 6 | 7 | sio = socketio.Server() 8 | app = Flask(__name__) 9 | 10 | connected_particpants = {} 11 | 12 | def write_log(s): 13 | with open('logfile.out', 'a+') as f: 14 | f.write('time: %s Action: %s \n' % (str(datetime.datetime.now()), s)) 15 | 16 | @app.route('/') 17 | def index(): 18 | """Serve index page""" 19 | return render_template('index.html', room='default') 20 | 21 | @sio.on('message', namespace='/') 22 | def messgage(sid, data): 23 | sio.emit('message', data=data) 24 | 25 | @sio.on('disconnect', namespace='/') 26 | def disconnect(sid): 27 | write_log("Received Disconnect message from %s" % sid) 28 | for room, clients in connected_particpants.iteritems(): 29 | try: 30 | clients.remove(sid) 31 | write_log("Removed %s from %s \n list of left participants is %s" %(sid, room, clients)) 32 | except ValueError: 33 | write_log("Remove %s from %s \n list of left participants is %s has failed" %(sid, room, clients)) 34 | 35 | @sio.on('create or join', namespace='/') 36 | def create_or_join(sid, data): 37 | sio.enter_room(sid, data) 38 | try: 39 | connected_particpants[data].append(sid) 40 | except KeyError: 41 | connected_particpants[data] = [sid] 42 | numClients = len(connected_particpants[data]) 43 | if numClients == 1: 44 | sio.emit('created', data) 45 | elif numClients > 2: 46 | sio.emit('full') 47 | elif numClients == 2: 48 | sio.emit('joined') 49 | sio.emit('join') 50 | print (sid, data, len(connected_particpants[data])) 51 | 52 | @app.route('/') 53 | def room(room): 54 | return render_template('index.html', room=room) 55 | 56 | if __name__ == '__main__': 57 | app = socketio.Middleware(sio, app) 58 | eventlet.wsgi.server(eventlet.listen(('', 8080)), app) 59 | --------------------------------------------------------------------------------