├── .gitignore ├── img ├── jack.gif ├── king.gif ├── queen.gif └── cardback.gif ├── index.html ├── package.json ├── scripts ├── peerx.js ├── deal.js ├── example.js ├── readQrCode.js ├── userMedia.js ├── bpg-cards.js └── peer.js ├── style.css ├── carddemo.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /img/jack.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicferrier/cards/master/img/jack.gif -------------------------------------------------------------------------------- /img/king.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicferrier/cards/master/img/king.gif -------------------------------------------------------------------------------- /img/queen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicferrier/cards/master/img/queen.gif -------------------------------------------------------------------------------- /img/cardback.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicferrier/cards/master/img/cardback.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 7 card stud 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bpg-playing-card", 3 | "version": "0.0.1", 4 | "description": "A playing card as a WebComponent. Easily make playing cards with HTML.", 5 | "keywords": ["webcomponent", "cards", "playingcard", "poker"], 6 | "homepage": "http://fn10vir1826.ivycomptech.co.in/Nic/carddeck-webc/tree/master", 7 | "license": "GPL-3.0+", 8 | "author": { 9 | "name": "Nic Ferrier", 10 | "email": "nicholas.ferrier@bwinparty.com", 11 | "url": "http://nic.ferrier.me.uk" 12 | }, 13 | "dependencies": { 14 | "polyfill-webcomponents": ">=0.20140621.0", 15 | "jsqrcode-lite": "0.1.0", 16 | "qrcode-npm": "0.0.3", 17 | "jquery-browserify": "1.8.1", 18 | "browserify": ">=4.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/peerx.js: -------------------------------------------------------------------------------- 1 | // Extensions to peer.js - this is just a grab bag utility extension module 2 | 3 | var p = require("./peer.js"); 4 | 5 | // Create a Peer mocking the peer.id from the document query if supplied 6 | var makePeerFromDocument = function (apiKey, peerIdValues) { 7 | if (document.location.search.indexOf("?master=") == 0) { 8 | peerIdValues.debugSlavePeerId 9 | = /[?&]master=([0-9]+)/.exec(document.location.search)[1]; 10 | return new p.Peer( 11 | peerIdValues.debugMasterPeerId, 12 | { key: apiKey, debug: 1 } 13 | ); 14 | } 15 | else if (document.location.search.indexOf("?slave") == 0) { 16 | peerIdValues.debugSlavePeerId 17 | = /[?&]slave=([0-9]+)/.exec(document.location.search)[1]; 18 | return new p.Peer( 19 | peerIdValues.debugSlavePeerId, 20 | { key: apiKey, debug: 1 } 21 | ); 22 | } 23 | else { 24 | return new p.Peer({ key: apiKey, debug: 1 }); 25 | } 26 | }; 27 | 28 | p.makePeer = makePeerFromDocument; 29 | module.exports = p; 30 | 31 | // peerx.js ends here 32 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "sans-serif"; 3 | padding-top: 10px; 4 | width: 420px; 5 | margin: 0 auto; 6 | } 7 | 8 | #console { 9 | font-size: 10pt; 10 | dispaly: none; 11 | } 12 | 13 | #connect { 14 | width: 420px; 15 | margin: 0 auto; 16 | padding-top: 20px; 17 | display: none; 18 | background-color: #aaaaaa; 19 | } 20 | 21 | #connectbox { 22 | width: 380px; 23 | height: 600px; 24 | margin: 0 auto; 25 | } 26 | 27 | #qrcode { 28 | padding-top: 20px; 29 | width: 350px; 30 | height: 225px; 31 | margin: 0 auto; 32 | text-align: center; 33 | } 34 | 35 | #vidCap { 36 | width: 350px; 37 | height: 225px; 38 | margin: 0 auto; 39 | display: block; 40 | align: center; 41 | } 42 | 43 | video { 44 | border: 3px solid #555555; 45 | width: 300px; 46 | height: 225px; 47 | margin: 0 auto; 48 | display: block; 49 | text-align: center; 50 | } 51 | 52 | #qr-canvas { 53 | display: none; 54 | width: 800px; 55 | height: 600px; 56 | } 57 | -------------------------------------------------------------------------------- /carddemo.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 24 | 25 | 27 | 28 | 30 | 31 | 33 | 34 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebComponents for Playing Cards 2 | =============================== 3 | 4 | This is an example of WebComponents elements for playing cards. 5 | 6 | The cards can be defined in HTML thusly: 7 | 8 | ```` 9 | 10 | 11 | ```` 12 | 13 | This will produce an Ace of Hearts and a Jack of Clubs. 14 | 15 | This may be the beginning of a useful library. 16 | 17 | 18 | ## Running 19 | Everything is packaged as a browserifyable node package. 20 | 21 | `npm install` this and then use browserify to serve it. 22 | 23 | To run browserify you need to use a server automatically running 24 | browserify on any ".js" file. I use elnode with it's convienient 25 | `elnode-js-make-webserver`. 26 | 27 | 28 | ## Using 29 | 30 | Include the js in your page: 31 | 32 | ```` 33 | 34 | 35 |

Poker!

36 | 37 | 38 | 39 | ```` 40 | 41 | Use from HTML like so: 42 | 43 | ```` 44 | 45 | 46 |

Poker!

47 | 48 | 49 | 50 | 51 | 52 | ```` 53 | 54 | You can also construct cards from JavaScript directly: 55 | 56 | ```` 57 | var bpg_cards = require ("./bpg-cards.js"); 58 | var PlayingCard = bpg_cards.card_init(document); 59 | var card = new PlayingCard(); 60 | card.setAttribute("value", "1"); 61 | card.setAttribute("suit", "hearts"); 62 | document.body.appendChild(elem); 63 | ```` 64 | 65 | ## What is this? 66 | 67 | It's the beginnings of an open library of poker elements. 68 | -------------------------------------------------------------------------------- /scripts/deal.js: -------------------------------------------------------------------------------- 1 | // deal cards 2 | 3 | // this clash detect algorithm sucks 4 | // how about this idea instead? 5 | // 6 | // var d = {}; for(var i = 0; i < 12; i++) {d[i]=i;} 7 | // 8 | // now choose d[random(d.length)] 9 | // then delete that index from d 10 | // keep one d for each suit 11 | // 12 | // so gradually the options reduce instead of maintaining the same 13 | // chance every time. 14 | 15 | 16 | var util = require("util"); 17 | 18 | // Like Emacs Lisp random 19 | function nic_random (n) { 20 | return Math.floor(n * Math.random()); 21 | } 22 | 23 | 24 | // Function to capture a deck and return a dealer for the deck 25 | function make_dealer () { 26 | var values = { 27 | 1: "1", 28 | 2: "2", 29 | 3: "3", 30 | 4: "4", 31 | 5: "5", 32 | 6: "6", 33 | 7: "7", 34 | 8: "8", 35 | 9: "9", 36 | 10: "10", 37 | 11: "J", 38 | 12: "Q", 39 | 13: "K" 40 | }; 41 | 42 | var suits = { 43 | 0: "clubs", 44 | 1: "diamonds", 45 | 2: "hearts", 46 | 3: "spades" 47 | }; 48 | 49 | var deck = {}; 50 | // Fill the deck 51 | for (var i = 0; i < 4; i++) { 52 | for (var k in values) { 53 | deck[util.format("%s,%s", suits[i], k)] = { 54 | "suit": suits[i], 55 | "value": values[k] 56 | }; 57 | } 58 | } 59 | 60 | var dealer = function () { 61 | var deck_keys = Object.keys(deck); 62 | if (deck_keys.length > 0) { 63 | var index = nic_random (deck_keys.length); 64 | var key = deck_keys [index]; 65 | var card = deck [key]; 66 | delete deck [key]; 67 | return card; 68 | } 69 | else { 70 | return {}; 71 | } 72 | }; 73 | return dealer; 74 | } 75 | 76 | test_deal = function () { 77 | var dealer = make_dealer (); 78 | for (var i = 0; i < 60; i++) { 79 | var card = dealer(); 80 | console.log(card); 81 | } 82 | } 83 | 84 | test_deal(); 85 | 86 | // End 87 | -------------------------------------------------------------------------------- /scripts/example.js: -------------------------------------------------------------------------------- 1 | // P2P poker 2 | 3 | var util = require("util"); 4 | var $ = require("jquery-browserify"); 5 | var qrCode = require("qrcode-npm"); 6 | var qrCodeRead = require("./readQrCode.js"); 7 | var PeerJs = require("./peerx.js"); 8 | 9 | var apiKey = "ntq9engji39grpb9"; 10 | var sessionId = 1; 11 | var peerIdDefaults = { 12 | debugMasterPeerId: 10001, 13 | debugSlavePeerId: 10002 14 | }; 15 | var peer = PeerJs.makePeer(apiKey, peerIdDefaults); 16 | var htmlLog = function (args) { 17 | if (arguments.length == 1) { 18 | $("#console").append(util.format("
  • %j
  • ", args)); 19 | } 20 | else { 21 | for (i in arguments) { 22 | var arg = arguments[i]; 23 | $("#console").append(util.format("
  • %j
  • ", arg)); 24 | } 25 | } 26 | }; 27 | 28 | var onData = function (dataCon, data) { 29 | var fmt = util.format("data: %s", data); 30 | console.log("onData", data); 31 | }; 32 | 33 | // What to do when a data connection is opened 34 | var onConOpen = function (dataCon, next) { 35 | console.log("onConOpen", dataCon); 36 | dataCon.on("data", function (data) { onData(dataCon, data); }); 37 | if (next) { next(); } 38 | }; 39 | 40 | // Receive a peerId and become the master or a slave via a QR 41 | var start = function (peerId) { 42 | // Make a qr code of the ID so we can give it to the other user 43 | var qr = qrCode.qrcode(4, 'M'); 44 | qr.addData(peerId); 45 | qr.make(); 46 | $("#qrcode").html(qr.createImgTag(8)); 47 | 48 | // Setup the camera to decode a QR code of the other side 49 | var abortCameraFn = qrCodeRead.capture( 50 | "vidCap", 51 | function (receivedQR) { 52 | $("#connect").toggle({duration: 400}); 53 | var dataConn = peer.connect(receivedQR); 54 | dataConn.on('open', onConOpen, function () { 55 | dataConn.send({ session: sessionId, 56 | verb: "GET", 57 | uri: "master|check" }); 58 | }); 59 | } 60 | ); 61 | 62 | // Listen from a connection from the QR decode side 63 | peer.on( 64 | "connection", 65 | function(dataConn) { 66 | $("#connect").toggle({duration: 400}); 67 | abortCameraFn(); 68 | dataConn.on("open", onConOpen); 69 | } 70 | ); 71 | 72 | // Open the connect display 73 | $("#connect").toggle({duration: 400}); 74 | }; 75 | 76 | 77 | // Initiate the peer connection to get a peer-id or use the debug stuff 78 | if (peer.id == peerIdDefaults.debugSlavePeerId) { 79 | var dataCon = peer.connect(peerIdDefaults.debugMasterPeerId); 80 | dataCon.on("open", function (dc) { 81 | onConOpen(dataCon, function () { 82 | dataCon.send({ session: sessionId, 83 | verb: "GET", 84 | uri: "master|check" }); 85 | }); 86 | }); 87 | } 88 | else if (peer.id == peerIdDefaults.debugMasterPeerId) { 89 | peer.on("connection", function (dc) { 90 | dc.on("open", function () { onConOpen(dc); }); 91 | }); 92 | } 93 | else { 94 | peer.on('open', start); 95 | } 96 | 97 | 98 | // And now the cards 99 | var bpg_cards = require ("./bpg-cards.js"); 100 | var PlayingCard = bpg_cards.card_init(document); 101 | 102 | 103 | // example.js ends here 104 | -------------------------------------------------------------------------------- /scripts/readQrCode.js: -------------------------------------------------------------------------------- 1 | // Read qr codes with the user-media web camera 2 | 3 | var decode = require('jsqrcode-lite'); 4 | var video = require("./userMedia.js"); 5 | 6 | var FINDER_DELAY = 1000; 7 | 8 | var initQRCanvas = function (receiveQR) { 9 | var canvasElement = document.createElement("canvas"); 10 | canvasElement.id = "qr-canvas"; 11 | document.body.appendChild(canvasElement);; 12 | // We could create this instead of refering to it 13 | var width = 800, height = 600; 14 | canvasElement.style.width = width + "px"; 15 | canvasElement.style.height = height + "px"; 16 | canvasElement.width = width; 17 | canvasElement.height = height; 18 | var gCtx = canvasElement.getContext("2d"); 19 | gCtx.clearRect(0, 0, width, height); 20 | 21 | // This function maps the videoObject and the canvas together 22 | var captureFunc = function(videoObject, captureTestObj) { 23 | try { 24 | gCtx.drawImage(videoObject, 0, 0); 25 | try { 26 | var dataBytes = gCtx.getImageData(0, 0, width, height).data; 27 | // This is a sucky way to do this, it would be better to alter 28 | // jsqrcode-lite to export more 29 | var args = { 30 | height: height, 31 | width: width, 32 | data: dataBytes 33 | }; 34 | decode( 35 | args, 36 | function (qrData) { 37 | // Turn off the video 38 | video.stopCapture(videoObject); 39 | // Call the user function 40 | receiveQR(qrData); 41 | } 42 | ); 43 | } 44 | catch(e){ 45 | console.log(e); 46 | if (captureTestObj.isOn) { 47 | setTimeout(function () { 48 | captureFunc(videoObject, captureTestObj); 49 | }, FINDER_DELAY); 50 | } 51 | else { 52 | video.stopCapture(videoObject); 53 | } 54 | }; 55 | } 56 | catch(e){ 57 | console.log(e); 58 | if (captureTestObj.isOn) { 59 | setTimeout(function () { 60 | captureFunc(videoObject, captureTestObj); 61 | }, FINDER_DELAY); 62 | } 63 | else { 64 | video.stopCapture(videoObject); 65 | } 66 | }; 67 | }; 68 | return captureFunc; 69 | } 70 | 71 | // Capture the QR code by making a video in the videoId div and send 72 | // the one argument to the receiveQR function 73 | exports.capture = function (videoId, receiveQR) { 74 | var captureToCanvasFn = initQRCanvas(receiveQR); 75 | // Now initialise the camera and when the video works have the QR code 76 | // reader kick in 77 | var vidDiv = document.getElementById(videoId); 78 | vidDiv.innerHTML = ""; 79 | var videoObject = document.getElementById("vid"); 80 | var captureOnObj = { isOn: true }; // closed var used to turn everything off 81 | video.doGetUserMedia( 82 | videoObject, 83 | // Success function is called when the video works 84 | function () { 85 | captureToCanvasFn(videoObject, captureOnObj); 86 | } 87 | ); 88 | // Return a function to turn the capture off 89 | var stopFunc = function () { 90 | console.log("turn off function called"); 91 | captureOnObj.isOn = false; 92 | }; 93 | return stopFunc; 94 | }; 95 | 96 | // readQrCode.js ends here 97 | -------------------------------------------------------------------------------- /scripts/userMedia.js: -------------------------------------------------------------------------------- 1 | // Abstract usermedia stuff a little 2 | var util = require("util"); 3 | 4 | // Find a camera, ideally an environment facing one, to capture the 5 | // QR code. 6 | // 7 | // Pass the id of the chosen stream to pickFn 8 | function getEnvironmentCamera (pickFn) { 9 | try { 10 | var mediaTrackFunc = MediaStreamTrack.getSources; 11 | if (typeof(mediaTrackFunc) != "function") { 12 | pickFn(); 13 | } 14 | else { 15 | mediaTrackFunc( 16 | function (sources) { 17 | var cameras = sources.filter( 18 | function (e) { 19 | return e.kind == "video"; 20 | }); 21 | 22 | // htmlLog("cameras:", cameras); 23 | 24 | var envFacing = cameras.filter( 25 | function (e) { 26 | return e.facing == "environment"; 27 | }); 28 | 29 | // htmlLog("away cameras:", envFacing); 30 | // console.log("env facing ", envFacing, "cameras ", cameras); 31 | 32 | if (typeof(pickFn) == "function") { 33 | if (envFacing.length > 0) { 34 | pickFn(envFacing[0].id); 35 | } 36 | else { 37 | // If there are no cameras we should error 38 | pickFn(cameras[0].id); 39 | } 40 | } 41 | }); 42 | } 43 | } 44 | catch (e) { 45 | console.log("userMedia.js ", e); 46 | } 47 | } 48 | 49 | function streamSuccess(stream, videoObject, browserType, successFn) { 50 | if(browserType == "webkit") { 51 | videoObject.src = window.webkitURL.createObjectURL(stream); 52 | } 53 | else { 54 | if(browserType == "moz") { 55 | videoObject.mozSrcObject = stream; 56 | videoObject.play(); 57 | } 58 | else { 59 | videoObject.src = stream; 60 | } 61 | } 62 | if (typeof(successFn) == "function") { 63 | setTimeout (successFn, 500); 64 | } 65 | } 66 | 67 | function error(error) { 68 | return; 69 | } 70 | 71 | // Take a video DOM element and turn on user media for it 72 | // 73 | // Optionally an onSuccess function which is called when the user 74 | // media stream works 75 | function getUserMediaWithStream(videoObject, onSuccess, streamId) { 76 | var constraints 77 | = ((streamId) ? 78 | { video: { optional: [{ sourceId: streamId }]}, audio: false } 79 | : { video: true, audio: false }); 80 | // htmlLog(constraints); 81 | if(navigator.getUserMedia) { 82 | navigator.getUserMedia( 83 | constraints, 84 | function (stream) { 85 | streamSuccess(stream, videoObject, "", onSuccess); 86 | }, 87 | error); 88 | } 89 | else { 90 | if(navigator.webkitGetUserMedia) { 91 | navigator.webkitGetUserMedia( 92 | constraints, 93 | function (stream) { 94 | streamSuccess(stream, videoObject, "webkit", onSuccess); 95 | }, 96 | error); 97 | } 98 | else { 99 | if(navigator.mozGetUserMedia) { 100 | navigator.mozGetUserMedia( 101 | constraints, 102 | function (stream) { 103 | streamSuccess(stream, videoObject, "moz", onSuccess); 104 | }, 105 | error); 106 | } 107 | } 108 | } 109 | } 110 | 111 | function doGetUserMedia (videoObject, onSuccess) { 112 | getEnvironmentCamera( 113 | function (selectedCamera) { 114 | // htmlLog("getUserMedia selected", selectedCamera); 115 | getUserMediaWithStream(videoObject, onSuccess, selectedCamera); 116 | } 117 | ); 118 | } 119 | 120 | function stopCapture (videoObject) { 121 | videoObject.pause(); 122 | try { 123 | videoObject.mozSrcObject = null; 124 | } 125 | catch (e) { 126 | try { 127 | videoObject.src = ""; 128 | } 129 | catch (e) { 130 | } 131 | } 132 | }; 133 | 134 | 135 | exports.doGetUserMedia = doGetUserMedia; 136 | exports.stopCapture = stopCapture; 137 | 138 | // userMedia.js ends here 139 | -------------------------------------------------------------------------------- /scripts/bpg-cards.js: -------------------------------------------------------------------------------- 1 | /* 2 | * BwinParty Playing Cards 3 | * 4 | * Copyright (C) 2014 by bwinparty 5 | * 6 | * This is an HTML5 WebComponent element for playing cards. 7 | * 8 | * The styles required for the WebComponent are embedded in the 9 | * document by this library. 10 | */ 11 | 12 | var Platform = require("polyfill-webcomponents"); 13 | var util = require("util"); 14 | 15 | var cssText = ".card {\ 16 | background-image: url('/img/cardback.gif');\ 17 | border-color: #808080 #000000 #000000 #808080;\ 18 | border-width: 1px;\ 19 | border-style: solid;\ 20 | color: #000000;\ 21 | font-size: 20pt;\ 22 | position: absolute;\ 23 | width: 3.75em;\ 24 | height: 5.00em;\ 25 | }\ 26 | \ 27 | .front {\ 28 | background-color: #ffffff;\ 29 | color: #000000;\ 30 | position: absolute;\ 31 | width: 100%;\ 32 | height: 100%;\ 33 | }\ 34 | \ 35 | .face {\ 36 | border: 1px solid #000000;\ 37 | position: absolute;\ 38 | left: 0.60em;\ 39 | top: 0.45em;\ 40 | width: 2.6em;\ 41 | height: 4.0em;\ 42 | }\ 43 | \ 44 | .red { \ 45 | color: #ff0000; \ 46 | }\ 47 | \ 48 | .index {\ 49 | font-size: 50%;\ 50 | font-weight: bold;\ 51 | text-align: center;\ 52 | position: absolute;\ 53 | left: 0.25em;\ 54 | top: 0.25em;\ 55 | }\ 56 | \ 57 | .ace {\ 58 | font-size: 300%;\ 59 | position: absolute;\ 60 | left: 0.225em;\ 61 | top: 0.250em;\ 62 | }\ 63 | \ 64 | .spotA1 { position: absolute; left: 0.60em; top: 0.5em; }\ 65 | .spotA2 { position: absolute; left: 0.60em; top: 1.5em; }\ 66 | .spotA3 { position: absolute; left: 0.60em; top: 2.0em; }\ 67 | .spotA4 { position: absolute; left: 0.60em; top: 2.5em; }\ 68 | .spotA5 { position: absolute; left: 0.60em; top: 3.5em; }\ 69 | .spotB1 { position: absolute; left: 1.55em; top: 0.5em; }\ 70 | .spotB2 { position: absolute; left: 1.55em; top: 1.0em; }\ 71 | .spotB3 { position: absolute; left: 1.55em; top: 2.0em; }\ 72 | .spotB4 { position: absolute; left: 1.55em; top: 3.0em; }\ 73 | .spotB5 { position: absolute; left: 1.55em; top: 3.5em; }\ 74 | .spotC1 { position: absolute; left: 2.50em; top: 0.5em; }\ 75 | .spotC2 { position: absolute; left: 2.50em; top: 1.5em; }\ 76 | .spotC3 { position: absolute; left: 2.50em; top: 2.0em; }\ 77 | .spotC4 { position: absolute; left: 2.50em; top: 2.5em; }\ 78 | .spotC5 { position: absolute; left: 2.50em; top: 3.5em; }\ 79 | "; 80 | 81 | // A JS example of using this... 82 | // var elem = new PlayingCard(); 83 | // document.body.appendChild(elem); 84 | 85 | var bpg_cards = 86 | (function () { 87 | // Setup the styles 88 | var styleE = document.createElement("style"); 89 | styleE.type = "text/css"; 90 | if (styleE.styleSheet) { 91 | styleE.styleSheet.cssText = cssText; 92 | } 93 | else { 94 | styleE.appendChild(document.createTextNode(cssText)); 95 | } 96 | (document.head || document.getElementsByTagName('head')[0]).appendChild(styleE); 97 | 98 | // Setup the element 99 | var playingCardType = Object.create(HTMLElement.prototype); 100 | playingCardType.createdCallback = function () { 101 | // Capture 'this' 102 | var self = this; 103 | 104 | // A quick access function 105 | var getAttr = function (name, defaultValue) { 106 | var val = self.getAttribute(name); 107 | return (val == null && defaultValue != null) ? defaultValue:val; 108 | }; 109 | 110 | // We need to cope with nulls in these attributes 111 | if (this.getAttribute("suit") && this.getAttribute("value")) { 112 | var suitEnt = { 113 | "clubs": "♣", 114 | "diamonds": "♦", 115 | "hearts": "♥", 116 | "spades": "♠" 117 | } [this.getAttribute("suit")]; 118 | 119 | var color = { 120 | "clubs": "black", 121 | "spades": "black", 122 | "hearts": "red", 123 | "diamonds": "red", 124 | }[this.getAttribute("suit")]; 125 | 126 | var side = getAttr("side", "front"); 127 | 128 | var value = { 129 | "1": "1", 130 | "2": "2", 131 | "3": "3", 132 | "4": "4", 133 | "5": "5", 134 | "6": "6", 135 | "7": "7", 136 | "8": "8", 137 | "9": "9", 138 | "10": "10", 139 | "11": "J", 140 | "12": "Q", 141 | "13": "K", 142 | "jack": "J", 143 | "queen": "Q", 144 | "king": "K", 145 | "J": "J", 146 | "Q": "Q", 147 | "K": "K" 148 | }[this.getAttribute("value")]; 149 | 150 | var valueMarkup = { 151 | "1": util.format("
    %s
    ", suitEnt), 152 | 153 | "2": util.format("
    %s
    ", suitEnt) 154 | + util.format("
    %s
    ", suitEnt), 155 | 156 | "3": util.format("
    %s
    ", suitEnt) 157 | + util.format("
    %s
    ", suitEnt) 158 | + util.format("
    %s
    ", suitEnt), 159 | 160 | "4": util.format("
    %s
    ", suitEnt) 161 | + util.format("
    %s
    ", suitEnt) 162 | + util.format("
    %s
    ", suitEnt) 163 | + util.format("
    %s
    ", suitEnt), 164 | 165 | "5": util.format("
    %s
    ", suitEnt) 166 | + util.format("
    %s
    ", suitEnt) 167 | + util.format("
    %s
    ", suitEnt) 168 | + util.format("
    %s
    ", suitEnt) 169 | + util.format("
    %s
    ", suitEnt), 170 | 171 | "6": util.format("
    %s
    ", suitEnt) 172 | + util.format("
    %s
    ", suitEnt) 173 | + util.format("
    %s
    ", suitEnt) 174 | + util.format("
    %s
    ", suitEnt) 175 | + util.format("
    %s
    ", suitEnt) 176 | + util.format("
    %s
    ", suitEnt), 177 | 178 | "7": util.format("
    %s
    ", suitEnt) 179 | + util.format("
    %s
    ", suitEnt) 180 | + util.format("
    %s
    ", suitEnt) 181 | + util.format("
    %s
    ", suitEnt) 182 | + util.format("
    %s
    ", suitEnt) 183 | + util.format("
    %s
    ", suitEnt) 184 | + util.format("
    %s
    ", suitEnt), 185 | 186 | "8": util.format("
    %s
    ", suitEnt) 187 | + util.format("
    %s
    ", suitEnt) 188 | + util.format("
    %s
    ", suitEnt) 189 | + util.format("
    %s
    ", suitEnt) 190 | + util.format("
    %s
    ", suitEnt) 191 | + util.format("
    %s
    ", suitEnt) 192 | + util.format("
    %s
    ", suitEnt) 193 | + util.format("
    %s
    ", suitEnt), 194 | 195 | "9": util.format("
    %s
    ", suitEnt) 196 | + util.format("
    %s
    ", suitEnt) 197 | + util.format("
    %s
    ", suitEnt) 198 | + util.format("
    %s
    ", suitEnt) 199 | + util.format("
    %s
    ", suitEnt) 200 | + util.format("
    %s
    ", suitEnt) 201 | + util.format("
    %s
    ", suitEnt) 202 | + util.format("
    %s
    ", suitEnt) 203 | + util.format("
    %s
    ", suitEnt), 204 | 205 | "10": util.format("
    %s
    ", suitEnt) 206 | + util.format("
    %s
    ", suitEnt) 207 | + util.format("
    %s
    ", suitEnt) 208 | + util.format("
    %s
    ", suitEnt) 209 | + util.format("
    %s
    ", suitEnt) 210 | + util.format("
    %s
    ", suitEnt) 211 | + util.format("
    %s
    ", suitEnt) 212 | + util.format("
    %s
    ", suitEnt) 213 | + util.format("
    %s
    ", suitEnt) 214 | + util.format("
    %s
    ", suitEnt), 215 | 216 | "J": "" 217 | + util.format("
    %s
    ", suitEnt) 218 | + util.format("
    %s
    ", suitEnt), 219 | "Q": "" 220 | + util.format("
    %s
    ", suitEnt) 221 | + util.format("
    %s
    ", suitEnt), 222 | "K": "" 223 | + util.format("
    %s
    ", suitEnt) 224 | + util.format("
    %s
    ", suitEnt) 225 | }[value]; 226 | 227 | this.innerHTML = util.format( 228 | "
    " 229 | + "
    " 230 | + "
    %s
    %s
    ", 231 | this.getAttribute("style"), 232 | side, color, 233 | (side == "back") ? "":value, (side=="back") ? "":valueMarkup); 234 | } 235 | }; 236 | 237 | // initialize the playing card lib 238 | var init = function (doc) { 239 | var PlayingCard = doc.registerElement( 240 | "playing-card", { prototype: playingCardType } 241 | ); 242 | return PlayingCard; 243 | }; 244 | 245 | return { 246 | "init": init 247 | }; 248 | })(); 249 | 250 | exports.card_init = bpg_cards.init; 251 | 252 | // example.js ends here 253 | -------------------------------------------------------------------------------- /scripts/peer.js: -------------------------------------------------------------------------------- 1 | /*! peerjs.js build:0.3.8, development. Copyright(c) 2013 Michelle Bu */ 2 | (function(exports){ 3 | var binaryFeatures = {}; 4 | binaryFeatures.useBlobBuilder = (function(){ 5 | try { 6 | new Blob([]); 7 | return false; 8 | } catch (e) { 9 | return true; 10 | } 11 | })(); 12 | 13 | binaryFeatures.useArrayBufferView = !binaryFeatures.useBlobBuilder && (function(){ 14 | try { 15 | return (new Blob([new Uint8Array([])])).size === 0; 16 | } catch (e) { 17 | return true; 18 | } 19 | })(); 20 | 21 | exports.binaryFeatures = binaryFeatures; 22 | exports.BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder || window.BlobBuilder; 23 | 24 | function BufferBuilder(){ 25 | this._pieces = []; 26 | this._parts = []; 27 | } 28 | 29 | BufferBuilder.prototype.append = function(data) { 30 | if(typeof data === 'number') { 31 | this._pieces.push(data); 32 | } else { 33 | this.flush(); 34 | this._parts.push(data); 35 | } 36 | }; 37 | 38 | BufferBuilder.prototype.flush = function() { 39 | if (this._pieces.length > 0) { 40 | var buf = new Uint8Array(this._pieces); 41 | if(!binaryFeatures.useArrayBufferView) { 42 | buf = buf.buffer; 43 | } 44 | this._parts.push(buf); 45 | this._pieces = []; 46 | } 47 | }; 48 | 49 | BufferBuilder.prototype.getBuffer = function() { 50 | this.flush(); 51 | if(binaryFeatures.useBlobBuilder) { 52 | var builder = new BlobBuilder(); 53 | for(var i = 0, ii = this._parts.length; i < ii; i++) { 54 | builder.append(this._parts[i]); 55 | } 56 | return builder.getBlob(); 57 | } else { 58 | return new Blob(this._parts); 59 | } 60 | }; 61 | 62 | exports.BinaryPack = { 63 | unpack: function(data){ 64 | var unpacker = new Unpacker(data); 65 | return unpacker.unpack(); 66 | }, 67 | pack: function(data){ 68 | var packer = new Packer(); 69 | packer.pack(data); 70 | var buffer = packer.getBuffer(); 71 | return buffer; 72 | } 73 | }; 74 | var BinaryPack = exports.BinaryPack; 75 | 76 | function Unpacker (data){ 77 | // Data is ArrayBuffer 78 | this.index = 0; 79 | this.dataBuffer = data; 80 | this.dataView = new Uint8Array(this.dataBuffer); 81 | this.length = this.dataBuffer.byteLength; 82 | } 83 | 84 | 85 | Unpacker.prototype.unpack = function(){ 86 | var type = this.unpack_uint8(); 87 | if (type < 0x80){ 88 | var positive_fixnum = type; 89 | return positive_fixnum; 90 | } else if ((type ^ 0xe0) < 0x20){ 91 | var negative_fixnum = (type ^ 0xe0) - 0x20; 92 | return negative_fixnum; 93 | } 94 | var size; 95 | if ((size = type ^ 0xa0) <= 0x0f){ 96 | return this.unpack_raw(size); 97 | } else if ((size = type ^ 0xb0) <= 0x0f){ 98 | return this.unpack_string(size); 99 | } else if ((size = type ^ 0x90) <= 0x0f){ 100 | return this.unpack_array(size); 101 | } else if ((size = type ^ 0x80) <= 0x0f){ 102 | return this.unpack_map(size); 103 | } 104 | switch(type){ 105 | case 0xc0: 106 | return null; 107 | case 0xc1: 108 | return undefined; 109 | case 0xc2: 110 | return false; 111 | case 0xc3: 112 | return true; 113 | case 0xca: 114 | return this.unpack_float(); 115 | case 0xcb: 116 | return this.unpack_double(); 117 | case 0xcc: 118 | return this.unpack_uint8(); 119 | case 0xcd: 120 | return this.unpack_uint16(); 121 | case 0xce: 122 | return this.unpack_uint32(); 123 | case 0xcf: 124 | return this.unpack_uint64(); 125 | case 0xd0: 126 | return this.unpack_int8(); 127 | case 0xd1: 128 | return this.unpack_int16(); 129 | case 0xd2: 130 | return this.unpack_int32(); 131 | case 0xd3: 132 | return this.unpack_int64(); 133 | case 0xd4: 134 | return undefined; 135 | case 0xd5: 136 | return undefined; 137 | case 0xd6: 138 | return undefined; 139 | case 0xd7: 140 | return undefined; 141 | case 0xd8: 142 | size = this.unpack_uint16(); 143 | return this.unpack_string(size); 144 | case 0xd9: 145 | size = this.unpack_uint32(); 146 | return this.unpack_string(size); 147 | case 0xda: 148 | size = this.unpack_uint16(); 149 | return this.unpack_raw(size); 150 | case 0xdb: 151 | size = this.unpack_uint32(); 152 | return this.unpack_raw(size); 153 | case 0xdc: 154 | size = this.unpack_uint16(); 155 | return this.unpack_array(size); 156 | case 0xdd: 157 | size = this.unpack_uint32(); 158 | return this.unpack_array(size); 159 | case 0xde: 160 | size = this.unpack_uint16(); 161 | return this.unpack_map(size); 162 | case 0xdf: 163 | size = this.unpack_uint32(); 164 | return this.unpack_map(size); 165 | } 166 | } 167 | 168 | Unpacker.prototype.unpack_uint8 = function(){ 169 | var byte = this.dataView[this.index] & 0xff; 170 | this.index++; 171 | return byte; 172 | }; 173 | 174 | Unpacker.prototype.unpack_uint16 = function(){ 175 | var bytes = this.read(2); 176 | var uint16 = 177 | ((bytes[0] & 0xff) * 256) + (bytes[1] & 0xff); 178 | this.index += 2; 179 | return uint16; 180 | } 181 | 182 | Unpacker.prototype.unpack_uint32 = function(){ 183 | var bytes = this.read(4); 184 | var uint32 = 185 | ((bytes[0] * 256 + 186 | bytes[1]) * 256 + 187 | bytes[2]) * 256 + 188 | bytes[3]; 189 | this.index += 4; 190 | return uint32; 191 | } 192 | 193 | Unpacker.prototype.unpack_uint64 = function(){ 194 | var bytes = this.read(8); 195 | var uint64 = 196 | ((((((bytes[0] * 256 + 197 | bytes[1]) * 256 + 198 | bytes[2]) * 256 + 199 | bytes[3]) * 256 + 200 | bytes[4]) * 256 + 201 | bytes[5]) * 256 + 202 | bytes[6]) * 256 + 203 | bytes[7]; 204 | this.index += 8; 205 | return uint64; 206 | } 207 | 208 | 209 | Unpacker.prototype.unpack_int8 = function(){ 210 | var uint8 = this.unpack_uint8(); 211 | return (uint8 < 0x80 ) ? uint8 : uint8 - (1 << 8); 212 | }; 213 | 214 | Unpacker.prototype.unpack_int16 = function(){ 215 | var uint16 = this.unpack_uint16(); 216 | return (uint16 < 0x8000 ) ? uint16 : uint16 - (1 << 16); 217 | } 218 | 219 | Unpacker.prototype.unpack_int32 = function(){ 220 | var uint32 = this.unpack_uint32(); 221 | return (uint32 < Math.pow(2, 31) ) ? uint32 : 222 | uint32 - Math.pow(2, 32); 223 | } 224 | 225 | Unpacker.prototype.unpack_int64 = function(){ 226 | var uint64 = this.unpack_uint64(); 227 | return (uint64 < Math.pow(2, 63) ) ? uint64 : 228 | uint64 - Math.pow(2, 64); 229 | } 230 | 231 | Unpacker.prototype.unpack_raw = function(size){ 232 | if ( this.length < this.index + size){ 233 | throw new Error('BinaryPackFailure: index is out of range' 234 | + ' ' + this.index + ' ' + size + ' ' + this.length); 235 | } 236 | var buf = this.dataBuffer.slice(this.index, this.index + size); 237 | this.index += size; 238 | 239 | //buf = util.bufferToString(buf); 240 | 241 | return buf; 242 | } 243 | 244 | Unpacker.prototype.unpack_string = function(size){ 245 | var bytes = this.read(size); 246 | var i = 0, str = '', c, code; 247 | while(i < size){ 248 | c = bytes[i]; 249 | if ( c < 128){ 250 | str += String.fromCharCode(c); 251 | i++; 252 | } else if ((c ^ 0xc0) < 32){ 253 | code = ((c ^ 0xc0) << 6) | (bytes[i+1] & 63); 254 | str += String.fromCharCode(code); 255 | i += 2; 256 | } else { 257 | code = ((c & 15) << 12) | ((bytes[i+1] & 63) << 6) | 258 | (bytes[i+2] & 63); 259 | str += String.fromCharCode(code); 260 | i += 3; 261 | } 262 | } 263 | this.index += size; 264 | return str; 265 | } 266 | 267 | Unpacker.prototype.unpack_array = function(size){ 268 | var objects = new Array(size); 269 | for(var i = 0; i < size ; i++){ 270 | objects[i] = this.unpack(); 271 | } 272 | return objects; 273 | } 274 | 275 | Unpacker.prototype.unpack_map = function(size){ 276 | var map = {}; 277 | for(var i = 0; i < size ; i++){ 278 | var key = this.unpack(); 279 | var value = this.unpack(); 280 | map[key] = value; 281 | } 282 | return map; 283 | } 284 | 285 | Unpacker.prototype.unpack_float = function(){ 286 | var uint32 = this.unpack_uint32(); 287 | var sign = uint32 >> 31; 288 | var exp = ((uint32 >> 23) & 0xff) - 127; 289 | var fraction = ( uint32 & 0x7fffff ) | 0x800000; 290 | return (sign == 0 ? 1 : -1) * 291 | fraction * Math.pow(2, exp - 23); 292 | } 293 | 294 | Unpacker.prototype.unpack_double = function(){ 295 | var h32 = this.unpack_uint32(); 296 | var l32 = this.unpack_uint32(); 297 | var sign = h32 >> 31; 298 | var exp = ((h32 >> 20) & 0x7ff) - 1023; 299 | var hfrac = ( h32 & 0xfffff ) | 0x100000; 300 | var frac = hfrac * Math.pow(2, exp - 20) + 301 | l32 * Math.pow(2, exp - 52); 302 | return (sign == 0 ? 1 : -1) * frac; 303 | } 304 | 305 | Unpacker.prototype.read = function(length){ 306 | var j = this.index; 307 | if (j + length <= this.length) { 308 | return this.dataView.subarray(j, j + length); 309 | } else { 310 | throw new Error('BinaryPackFailure: read index out of range'); 311 | } 312 | } 313 | 314 | function Packer(){ 315 | this.bufferBuilder = new BufferBuilder(); 316 | } 317 | 318 | Packer.prototype.getBuffer = function(){ 319 | return this.bufferBuilder.getBuffer(); 320 | } 321 | 322 | Packer.prototype.pack = function(value){ 323 | var type = typeof(value); 324 | if (type == 'string'){ 325 | this.pack_string(value); 326 | } else if (type == 'number'){ 327 | if (Math.floor(value) === value){ 328 | this.pack_integer(value); 329 | } else{ 330 | this.pack_double(value); 331 | } 332 | } else if (type == 'boolean'){ 333 | if (value === true){ 334 | this.bufferBuilder.append(0xc3); 335 | } else if (value === false){ 336 | this.bufferBuilder.append(0xc2); 337 | } 338 | } else if (type == 'undefined'){ 339 | this.bufferBuilder.append(0xc0); 340 | } else if (type == 'object'){ 341 | if (value === null){ 342 | this.bufferBuilder.append(0xc0); 343 | } else { 344 | var constructor = value.constructor; 345 | if (constructor == Array){ 346 | this.pack_array(value); 347 | } else if (constructor == Blob || constructor == File) { 348 | this.pack_bin(value); 349 | } else if (constructor == ArrayBuffer) { 350 | if(binaryFeatures.useArrayBufferView) { 351 | this.pack_bin(new Uint8Array(value)); 352 | } else { 353 | this.pack_bin(value); 354 | } 355 | } else if ('BYTES_PER_ELEMENT' in value){ 356 | if(binaryFeatures.useArrayBufferView) { 357 | this.pack_bin(new Uint8Array(value.buffer)); 358 | } else { 359 | this.pack_bin(value.buffer); 360 | } 361 | } else if (constructor == Object){ 362 | this.pack_object(value); 363 | } else if (constructor == Date){ 364 | this.pack_string(value.toString()); 365 | } else if (typeof value.toBinaryPack == 'function'){ 366 | this.bufferBuilder.append(value.toBinaryPack()); 367 | } else { 368 | throw new Error('Type "' + constructor.toString() + '" not yet supported'); 369 | } 370 | } 371 | } else { 372 | throw new Error('Type "' + type + '" not yet supported'); 373 | } 374 | this.bufferBuilder.flush(); 375 | } 376 | 377 | 378 | Packer.prototype.pack_bin = function(blob){ 379 | var length = blob.length || blob.byteLength || blob.size; 380 | if (length <= 0x0f){ 381 | this.pack_uint8(0xa0 + length); 382 | } else if (length <= 0xffff){ 383 | this.bufferBuilder.append(0xda) ; 384 | this.pack_uint16(length); 385 | } else if (length <= 0xffffffff){ 386 | this.bufferBuilder.append(0xdb); 387 | this.pack_uint32(length); 388 | } else{ 389 | throw new Error('Invalid length'); 390 | return; 391 | } 392 | this.bufferBuilder.append(blob); 393 | } 394 | 395 | Packer.prototype.pack_string = function(str){ 396 | var length = utf8Length(str); 397 | 398 | if (length <= 0x0f){ 399 | this.pack_uint8(0xb0 + length); 400 | } else if (length <= 0xffff){ 401 | this.bufferBuilder.append(0xd8) ; 402 | this.pack_uint16(length); 403 | } else if (length <= 0xffffffff){ 404 | this.bufferBuilder.append(0xd9); 405 | this.pack_uint32(length); 406 | } else{ 407 | throw new Error('Invalid length'); 408 | return; 409 | } 410 | this.bufferBuilder.append(str); 411 | } 412 | 413 | Packer.prototype.pack_array = function(ary){ 414 | var length = ary.length; 415 | if (length <= 0x0f){ 416 | this.pack_uint8(0x90 + length); 417 | } else if (length <= 0xffff){ 418 | this.bufferBuilder.append(0xdc) 419 | this.pack_uint16(length); 420 | } else if (length <= 0xffffffff){ 421 | this.bufferBuilder.append(0xdd); 422 | this.pack_uint32(length); 423 | } else{ 424 | throw new Error('Invalid length'); 425 | } 426 | for(var i = 0; i < length ; i++){ 427 | this.pack(ary[i]); 428 | } 429 | } 430 | 431 | Packer.prototype.pack_integer = function(num){ 432 | if ( -0x20 <= num && num <= 0x7f){ 433 | this.bufferBuilder.append(num & 0xff); 434 | } else if (0x00 <= num && num <= 0xff){ 435 | this.bufferBuilder.append(0xcc); 436 | this.pack_uint8(num); 437 | } else if (-0x80 <= num && num <= 0x7f){ 438 | this.bufferBuilder.append(0xd0); 439 | this.pack_int8(num); 440 | } else if ( 0x0000 <= num && num <= 0xffff){ 441 | this.bufferBuilder.append(0xcd); 442 | this.pack_uint16(num); 443 | } else if (-0x8000 <= num && num <= 0x7fff){ 444 | this.bufferBuilder.append(0xd1); 445 | this.pack_int16(num); 446 | } else if ( 0x00000000 <= num && num <= 0xffffffff){ 447 | this.bufferBuilder.append(0xce); 448 | this.pack_uint32(num); 449 | } else if (-0x80000000 <= num && num <= 0x7fffffff){ 450 | this.bufferBuilder.append(0xd2); 451 | this.pack_int32(num); 452 | } else if (-0x8000000000000000 <= num && num <= 0x7FFFFFFFFFFFFFFF){ 453 | this.bufferBuilder.append(0xd3); 454 | this.pack_int64(num); 455 | } else if (0x0000000000000000 <= num && num <= 0xFFFFFFFFFFFFFFFF){ 456 | this.bufferBuilder.append(0xcf); 457 | this.pack_uint64(num); 458 | } else{ 459 | throw new Error('Invalid integer'); 460 | } 461 | } 462 | 463 | Packer.prototype.pack_double = function(num){ 464 | var sign = 0; 465 | if (num < 0){ 466 | sign = 1; 467 | num = -num; 468 | } 469 | var exp = Math.floor(Math.log(num) / Math.LN2); 470 | var frac0 = num / Math.pow(2, exp) - 1; 471 | var frac1 = Math.floor(frac0 * Math.pow(2, 52)); 472 | var b32 = Math.pow(2, 32); 473 | var h32 = (sign << 31) | ((exp+1023) << 20) | 474 | (frac1 / b32) & 0x0fffff; 475 | var l32 = frac1 % b32; 476 | this.bufferBuilder.append(0xcb); 477 | this.pack_int32(h32); 478 | this.pack_int32(l32); 479 | } 480 | 481 | Packer.prototype.pack_object = function(obj){ 482 | var keys = Object.keys(obj); 483 | var length = keys.length; 484 | if (length <= 0x0f){ 485 | this.pack_uint8(0x80 + length); 486 | } else if (length <= 0xffff){ 487 | this.bufferBuilder.append(0xde); 488 | this.pack_uint16(length); 489 | } else if (length <= 0xffffffff){ 490 | this.bufferBuilder.append(0xdf); 491 | this.pack_uint32(length); 492 | } else{ 493 | throw new Error('Invalid length'); 494 | } 495 | for(var prop in obj){ 496 | if (obj.hasOwnProperty(prop)){ 497 | this.pack(prop); 498 | this.pack(obj[prop]); 499 | } 500 | } 501 | } 502 | 503 | Packer.prototype.pack_uint8 = function(num){ 504 | this.bufferBuilder.append(num); 505 | } 506 | 507 | Packer.prototype.pack_uint16 = function(num){ 508 | this.bufferBuilder.append(num >> 8); 509 | this.bufferBuilder.append(num & 0xff); 510 | } 511 | 512 | Packer.prototype.pack_uint32 = function(num){ 513 | var n = num & 0xffffffff; 514 | this.bufferBuilder.append((n & 0xff000000) >>> 24); 515 | this.bufferBuilder.append((n & 0x00ff0000) >>> 16); 516 | this.bufferBuilder.append((n & 0x0000ff00) >>> 8); 517 | this.bufferBuilder.append((n & 0x000000ff)); 518 | } 519 | 520 | Packer.prototype.pack_uint64 = function(num){ 521 | var high = num / Math.pow(2, 32); 522 | var low = num % Math.pow(2, 32); 523 | this.bufferBuilder.append((high & 0xff000000) >>> 24); 524 | this.bufferBuilder.append((high & 0x00ff0000) >>> 16); 525 | this.bufferBuilder.append((high & 0x0000ff00) >>> 8); 526 | this.bufferBuilder.append((high & 0x000000ff)); 527 | this.bufferBuilder.append((low & 0xff000000) >>> 24); 528 | this.bufferBuilder.append((low & 0x00ff0000) >>> 16); 529 | this.bufferBuilder.append((low & 0x0000ff00) >>> 8); 530 | this.bufferBuilder.append((low & 0x000000ff)); 531 | } 532 | 533 | Packer.prototype.pack_int8 = function(num){ 534 | this.bufferBuilder.append(num & 0xff); 535 | } 536 | 537 | Packer.prototype.pack_int16 = function(num){ 538 | this.bufferBuilder.append((num & 0xff00) >> 8); 539 | this.bufferBuilder.append(num & 0xff); 540 | } 541 | 542 | Packer.prototype.pack_int32 = function(num){ 543 | this.bufferBuilder.append((num >>> 24) & 0xff); 544 | this.bufferBuilder.append((num & 0x00ff0000) >>> 16); 545 | this.bufferBuilder.append((num & 0x0000ff00) >>> 8); 546 | this.bufferBuilder.append((num & 0x000000ff)); 547 | } 548 | 549 | Packer.prototype.pack_int64 = function(num){ 550 | var high = Math.floor(num / Math.pow(2, 32)); 551 | var low = num % Math.pow(2, 32); 552 | this.bufferBuilder.append((high & 0xff000000) >>> 24); 553 | this.bufferBuilder.append((high & 0x00ff0000) >>> 16); 554 | this.bufferBuilder.append((high & 0x0000ff00) >>> 8); 555 | this.bufferBuilder.append((high & 0x000000ff)); 556 | this.bufferBuilder.append((low & 0xff000000) >>> 24); 557 | this.bufferBuilder.append((low & 0x00ff0000) >>> 16); 558 | this.bufferBuilder.append((low & 0x0000ff00) >>> 8); 559 | this.bufferBuilder.append((low & 0x000000ff)); 560 | } 561 | 562 | function _utf8Replace(m){ 563 | var code = m.charCodeAt(0); 564 | 565 | if(code <= 0x7ff) return '00'; 566 | if(code <= 0xffff) return '000'; 567 | if(code <= 0x1fffff) return '0000'; 568 | if(code <= 0x3ffffff) return '00000'; 569 | return '000000'; 570 | } 571 | 572 | function utf8Length(str){ 573 | if (str.length > 600) { 574 | // Blob method faster for large strings 575 | return (new Blob([str])).size; 576 | } else { 577 | return str.replace(/[^\u0000-\u007F]/g, _utf8Replace).length; 578 | } 579 | } 580 | /** 581 | * Light EventEmitter. Ported from Node.js/events.js 582 | * Eric Zhang 583 | */ 584 | 585 | /** 586 | * EventEmitter class 587 | * Creates an object with event registering and firing methods 588 | */ 589 | function EventEmitter() { 590 | // Initialise required storage variables 591 | this._events = {}; 592 | } 593 | 594 | var isArray = Array.isArray; 595 | 596 | 597 | EventEmitter.prototype.addListener = function(type, listener, scope, once) { 598 | if ('function' !== typeof listener) { 599 | throw new Error('addListener only takes instances of Function'); 600 | } 601 | 602 | // To avoid recursion in the case that type == "newListeners"! Before 603 | // adding it to the listeners, first emit "newListeners". 604 | this.emit('newListener', type, typeof listener.listener === 'function' ? 605 | listener.listener : listener); 606 | 607 | if (!this._events[type]) { 608 | // Optimize the case of one listener. Don't need the extra array object. 609 | this._events[type] = listener; 610 | } else if (isArray(this._events[type])) { 611 | 612 | // If we've already got an array, just append. 613 | this._events[type].push(listener); 614 | 615 | } else { 616 | // Adding the second element, need to change to array. 617 | this._events[type] = [this._events[type], listener]; 618 | } 619 | return this; 620 | }; 621 | 622 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 623 | 624 | EventEmitter.prototype.once = function(type, listener, scope) { 625 | if ('function' !== typeof listener) { 626 | throw new Error('.once only takes instances of Function'); 627 | } 628 | 629 | var self = this; 630 | function g() { 631 | self.removeListener(type, g); 632 | listener.apply(this, arguments); 633 | }; 634 | 635 | g.listener = listener; 636 | self.on(type, g); 637 | 638 | return this; 639 | }; 640 | 641 | EventEmitter.prototype.removeListener = function(type, listener, scope) { 642 | if ('function' !== typeof listener) { 643 | throw new Error('removeListener only takes instances of Function'); 644 | } 645 | 646 | // does not use listeners(), so no side effect of creating _events[type] 647 | if (!this._events[type]) return this; 648 | 649 | var list = this._events[type]; 650 | 651 | if (isArray(list)) { 652 | var position = -1; 653 | for (var i = 0, length = list.length; i < length; i++) { 654 | if (list[i] === listener || 655 | (list[i].listener && list[i].listener === listener)) 656 | { 657 | position = i; 658 | break; 659 | } 660 | } 661 | 662 | if (position < 0) return this; 663 | list.splice(position, 1); 664 | if (list.length == 0) 665 | delete this._events[type]; 666 | } else if (list === listener || 667 | (list.listener && list.listener === listener)) 668 | { 669 | delete this._events[type]; 670 | } 671 | 672 | return this; 673 | }; 674 | 675 | 676 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 677 | 678 | 679 | EventEmitter.prototype.removeAllListeners = function(type) { 680 | if (arguments.length === 0) { 681 | this._events = {}; 682 | return this; 683 | } 684 | 685 | // does not use listeners(), so no side effect of creating _events[type] 686 | if (type && this._events && this._events[type]) this._events[type] = null; 687 | return this; 688 | }; 689 | 690 | EventEmitter.prototype.listeners = function(type) { 691 | if (!this._events[type]) this._events[type] = []; 692 | if (!isArray(this._events[type])) { 693 | this._events[type] = [this._events[type]]; 694 | } 695 | return this._events[type]; 696 | }; 697 | 698 | EventEmitter.prototype.emit = function(type) { 699 | var type = arguments[0]; 700 | var handler = this._events[type]; 701 | if (!handler) return false; 702 | 703 | if (typeof handler == 'function') { 704 | switch (arguments.length) { 705 | // fast cases 706 | case 1: 707 | handler.call(this); 708 | break; 709 | case 2: 710 | handler.call(this, arguments[1]); 711 | break; 712 | case 3: 713 | handler.call(this, arguments[1], arguments[2]); 714 | break; 715 | // slower 716 | default: 717 | var l = arguments.length; 718 | var args = new Array(l - 1); 719 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 720 | handler.apply(this, args); 721 | } 722 | return true; 723 | 724 | } else if (isArray(handler)) { 725 | var l = arguments.length; 726 | var args = new Array(l - 1); 727 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 728 | 729 | var listeners = handler.slice(); 730 | for (var i = 0, l = listeners.length; i < l; i++) { 731 | listeners[i].apply(this, args); 732 | } 733 | return true; 734 | } else { 735 | return false; 736 | } 737 | }; 738 | 739 | 740 | 741 | /** 742 | * Reliable transfer for Chrome Canary DataChannel impl. 743 | * Author: @michellebu 744 | */ 745 | function Reliable(dc, debug) { 746 | if (!(this instanceof Reliable)) return new Reliable(dc); 747 | this._dc = dc; 748 | 749 | util.debug = debug; 750 | 751 | // Messages sent/received so far. 752 | // id: { ack: n, chunks: [...] } 753 | this._outgoing = {}; 754 | // id: { ack: ['ack', id, n], chunks: [...] } 755 | this._incoming = {}; 756 | this._received = {}; 757 | 758 | // Window size. 759 | this._window = 1000; 760 | // MTU. 761 | this._mtu = 500; 762 | // Interval for setInterval. In ms. 763 | this._interval = 0; 764 | 765 | // Messages sent. 766 | this._count = 0; 767 | 768 | // Outgoing message queue. 769 | this._queue = []; 770 | 771 | this._setupDC(); 772 | }; 773 | 774 | // Send a message reliably. 775 | Reliable.prototype.send = function(msg) { 776 | // Determine if chunking is necessary. 777 | var bl = util.pack(msg); 778 | if (bl.size < this._mtu) { 779 | this._handleSend(['no', bl]); 780 | return; 781 | } 782 | 783 | this._outgoing[this._count] = { 784 | ack: 0, 785 | chunks: this._chunk(bl) 786 | }; 787 | 788 | if (util.debug) { 789 | this._outgoing[this._count].timer = new Date(); 790 | } 791 | 792 | // Send prelim window. 793 | this._sendWindowedChunks(this._count); 794 | this._count += 1; 795 | }; 796 | 797 | // Set up interval for processing queue. 798 | Reliable.prototype._setupInterval = function() { 799 | // TODO: fail gracefully. 800 | 801 | var self = this; 802 | this._timeout = setInterval(function() { 803 | // FIXME: String stuff makes things terribly async. 804 | var msg = self._queue.shift(); 805 | if (msg._multiple) { 806 | for (var i = 0, ii = msg.length; i < ii; i += 1) { 807 | self._intervalSend(msg[i]); 808 | } 809 | } else { 810 | self._intervalSend(msg); 811 | } 812 | }, this._interval); 813 | }; 814 | 815 | Reliable.prototype._intervalSend = function(msg) { 816 | var self = this; 817 | msg = util.pack(msg); 818 | util.blobToBinaryString(msg, function(str) { 819 | self._dc.send(str); 820 | }); 821 | if (self._queue.length === 0) { 822 | clearTimeout(self._timeout); 823 | self._timeout = null; 824 | //self._processAcks(); 825 | } 826 | }; 827 | 828 | // Go through ACKs to send missing pieces. 829 | Reliable.prototype._processAcks = function() { 830 | for (var id in this._outgoing) { 831 | if (this._outgoing.hasOwnProperty(id)) { 832 | this._sendWindowedChunks(id); 833 | } 834 | } 835 | }; 836 | 837 | // Handle sending a message. 838 | // FIXME: Don't wait for interval time for all messages... 839 | Reliable.prototype._handleSend = function(msg) { 840 | var push = true; 841 | for (var i = 0, ii = this._queue.length; i < ii; i += 1) { 842 | var item = this._queue[i]; 843 | if (item === msg) { 844 | push = false; 845 | } else if (item._multiple && item.indexOf(msg) !== -1) { 846 | push = false; 847 | } 848 | } 849 | if (push) { 850 | this._queue.push(msg); 851 | if (!this._timeout) { 852 | this._setupInterval(); 853 | } 854 | } 855 | }; 856 | 857 | // Set up DataChannel handlers. 858 | Reliable.prototype._setupDC = function() { 859 | // Handle various message types. 860 | var self = this; 861 | this._dc.onmessage = function(e) { 862 | var msg = e.data; 863 | var datatype = msg.constructor; 864 | // FIXME: msg is String until binary is supported. 865 | // Once that happens, this will have to be smarter. 866 | if (datatype === String) { 867 | var ab = util.binaryStringToArrayBuffer(msg); 868 | msg = util.unpack(ab); 869 | self._handleMessage(msg); 870 | } 871 | }; 872 | }; 873 | 874 | // Handles an incoming message. 875 | Reliable.prototype._handleMessage = function(msg) { 876 | var id = msg[1]; 877 | var idata = this._incoming[id]; 878 | var odata = this._outgoing[id]; 879 | var data; 880 | switch (msg[0]) { 881 | // No chunking was done. 882 | case 'no': 883 | var message = id; 884 | if (!!message) { 885 | this.onmessage(util.unpack(message)); 886 | } 887 | break; 888 | // Reached the end of the message. 889 | case 'end': 890 | data = idata; 891 | 892 | // In case end comes first. 893 | this._received[id] = msg[2]; 894 | 895 | if (!data) { 896 | break; 897 | } 898 | 899 | this._ack(id); 900 | break; 901 | case 'ack': 902 | data = odata; 903 | if (!!data) { 904 | var ack = msg[2]; 905 | // Take the larger ACK, for out of order messages. 906 | data.ack = Math.max(ack, data.ack); 907 | 908 | // Clean up when all chunks are ACKed. 909 | if (data.ack >= data.chunks.length) { 910 | util.log('Time: ', new Date() - data.timer); 911 | delete this._outgoing[id]; 912 | } else { 913 | this._processAcks(); 914 | } 915 | } 916 | // If !data, just ignore. 917 | break; 918 | // Received a chunk of data. 919 | case 'chunk': 920 | // Create a new entry if none exists. 921 | data = idata; 922 | if (!data) { 923 | var end = this._received[id]; 924 | if (end === true) { 925 | break; 926 | } 927 | data = { 928 | ack: ['ack', id, 0], 929 | chunks: [] 930 | }; 931 | this._incoming[id] = data; 932 | } 933 | 934 | var n = msg[2]; 935 | var chunk = msg[3]; 936 | data.chunks[n] = new Uint8Array(chunk); 937 | 938 | // If we get the chunk we're looking for, ACK for next missing. 939 | // Otherwise, ACK the same N again. 940 | if (n === data.ack[2]) { 941 | this._calculateNextAck(id); 942 | } 943 | this._ack(id); 944 | break; 945 | default: 946 | // Shouldn't happen, but would make sense for message to just go 947 | // through as is. 948 | this._handleSend(msg); 949 | break; 950 | } 951 | }; 952 | 953 | // Chunks BL into smaller messages. 954 | Reliable.prototype._chunk = function(bl) { 955 | var chunks = []; 956 | var size = bl.size; 957 | var start = 0; 958 | while (start < size) { 959 | var end = Math.min(size, start + this._mtu); 960 | var b = bl.slice(start, end); 961 | var chunk = { 962 | payload: b 963 | } 964 | chunks.push(chunk); 965 | start = end; 966 | } 967 | util.log('Created', chunks.length, 'chunks.'); 968 | return chunks; 969 | }; 970 | 971 | // Sends ACK N, expecting Nth blob chunk for message ID. 972 | Reliable.prototype._ack = function(id) { 973 | var ack = this._incoming[id].ack; 974 | 975 | // if ack is the end value, then call _complete. 976 | if (this._received[id] === ack[2]) { 977 | this._complete(id); 978 | this._received[id] = true; 979 | } 980 | 981 | this._handleSend(ack); 982 | }; 983 | 984 | // Calculates the next ACK number, given chunks. 985 | Reliable.prototype._calculateNextAck = function(id) { 986 | var data = this._incoming[id]; 987 | var chunks = data.chunks; 988 | for (var i = 0, ii = chunks.length; i < ii; i += 1) { 989 | // This chunk is missing!!! Better ACK for it. 990 | if (chunks[i] === undefined) { 991 | data.ack[2] = i; 992 | return; 993 | } 994 | } 995 | data.ack[2] = chunks.length; 996 | }; 997 | 998 | // Sends the next window of chunks. 999 | Reliable.prototype._sendWindowedChunks = function(id) { 1000 | util.log('sendWindowedChunks for: ', id); 1001 | var data = this._outgoing[id]; 1002 | var ch = data.chunks; 1003 | var chunks = []; 1004 | var limit = Math.min(data.ack + this._window, ch.length); 1005 | for (var i = data.ack; i < limit; i += 1) { 1006 | if (!ch[i].sent || i === data.ack) { 1007 | ch[i].sent = true; 1008 | chunks.push(['chunk', id, i, ch[i].payload]); 1009 | } 1010 | } 1011 | if (data.ack + this._window >= ch.length) { 1012 | chunks.push(['end', id, ch.length]) 1013 | } 1014 | chunks._multiple = true; 1015 | this._handleSend(chunks); 1016 | }; 1017 | 1018 | // Puts together a message from chunks. 1019 | Reliable.prototype._complete = function(id) { 1020 | util.log('Completed called for', id); 1021 | var self = this; 1022 | var chunks = this._incoming[id].chunks; 1023 | var bl = new Blob(chunks); 1024 | util.blobToArrayBuffer(bl, function(ab) { 1025 | self.onmessage(util.unpack(ab)); 1026 | }); 1027 | delete this._incoming[id]; 1028 | }; 1029 | 1030 | // Ups bandwidth limit on SDP. Meant to be called during offer/answer. 1031 | Reliable.higherBandwidthSDP = function(sdp) { 1032 | // AS stands for Application-Specific Maximum. 1033 | // Bandwidth number is in kilobits / sec. 1034 | // See RFC for more info: http://www.ietf.org/rfc/rfc2327.txt 1035 | 1036 | // Chrome 31+ doesn't want us munging the SDP, so we'll let them have their 1037 | // way. 1038 | var version = navigator.appVersion.match(/Chrome\/(.*?) /); 1039 | if (version) { 1040 | version = parseInt(version[1].split('.').shift()); 1041 | if (version < 31) { 1042 | var parts = sdp.split('b=AS:30'); 1043 | var replace = 'b=AS:102400'; // 100 Mbps 1044 | if (parts.length > 1) { 1045 | return parts[0] + replace + parts[1]; 1046 | } 1047 | } 1048 | } 1049 | 1050 | return sdp; 1051 | }; 1052 | 1053 | // Overwritten, typically. 1054 | Reliable.prototype.onmessage = function(msg) {}; 1055 | 1056 | exports.Reliable = Reliable; 1057 | exports.RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; 1058 | exports.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.RTCPeerConnection; 1059 | exports.RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; 1060 | 1061 | var RTCSessionDescription = exports.RTCSessionDescription; 1062 | var RTCPeerConnection = exports.RTCPeerConnection; 1063 | var RTCIceCandidate = exports.RTCIceCandidate; 1064 | 1065 | var defaultConfig = {'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }]}; 1066 | var dataCount = 1; 1067 | 1068 | var util = { 1069 | noop: function() {}, 1070 | 1071 | CLOUD_HOST: '0.peerjs.com', 1072 | CLOUD_PORT: 9000, 1073 | 1074 | // Browsers that need chunking: 1075 | chunkedBrowsers: {'Chrome': 1}, 1076 | chunkedMTU: 16300, // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually. 1077 | 1078 | // Logging logic 1079 | logLevel: 0, 1080 | setLogLevel: function(level) { 1081 | var debugLevel = parseInt(level, 10); 1082 | if (!isNaN(parseInt(level, 10))) { 1083 | util.logLevel = debugLevel; 1084 | } else { 1085 | // If they are using truthy/falsy values for debug 1086 | util.logLevel = level ? 3 : 0; 1087 | } 1088 | util.log = util.warn = util.error = util.noop; 1089 | if (util.logLevel > 0) { 1090 | util.error = util._printWith('ERROR'); 1091 | } 1092 | if (util.logLevel > 1) { 1093 | util.warn = util._printWith('WARNING'); 1094 | } 1095 | if (util.logLevel > 2) { 1096 | util.log = util._print; 1097 | } 1098 | }, 1099 | setLogFunction: function(fn) { 1100 | if (fn.constructor !== Function) { 1101 | util.warn('The log function you passed in is not a function. Defaulting to regular logs.'); 1102 | } else { 1103 | util._print = fn; 1104 | } 1105 | }, 1106 | 1107 | _printWith: function(prefix) { 1108 | return function() { 1109 | var copy = Array.prototype.slice.call(arguments); 1110 | copy.unshift(prefix); 1111 | util._print.apply(util, copy); 1112 | }; 1113 | }, 1114 | _print: function () { 1115 | var err = false; 1116 | var copy = Array.prototype.slice.call(arguments); 1117 | copy.unshift('PeerJS: '); 1118 | for (var i = 0, l = copy.length; i < l; i++){ 1119 | if (copy[i] instanceof Error) { 1120 | copy[i] = '(' + copy[i].name + ') ' + copy[i].message; 1121 | err = true; 1122 | } 1123 | } 1124 | err ? console.error.apply(console, copy) : console.log.apply(console, copy); 1125 | }, 1126 | // 1127 | 1128 | // Returns browser-agnostic default config 1129 | defaultConfig: defaultConfig, 1130 | // 1131 | 1132 | // Returns the current browser. 1133 | browser: (function() { 1134 | if (window.mozRTCPeerConnection) { 1135 | return 'Firefox'; 1136 | } else if (window.webkitRTCPeerConnection) { 1137 | return 'Chrome'; 1138 | } else if (window.RTCPeerConnection) { 1139 | return 'Supported'; 1140 | } else { 1141 | return 'Unsupported'; 1142 | } 1143 | })(), 1144 | // 1145 | 1146 | // Lists which features are supported 1147 | supports: (function() { 1148 | if (typeof RTCPeerConnection === 'undefined') { 1149 | return {}; 1150 | } 1151 | 1152 | var data = true; 1153 | var audioVideo = true; 1154 | 1155 | var binaryBlob = false; 1156 | var sctp = false; 1157 | var onnegotiationneeded = !!window.webkitRTCPeerConnection; 1158 | 1159 | var pc, dc; 1160 | try { 1161 | pc = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]}); 1162 | } catch (e) { 1163 | data = false; 1164 | audioVideo = false; 1165 | } 1166 | 1167 | if (data) { 1168 | try { 1169 | dc = pc.createDataChannel('_PEERJSTEST'); 1170 | } catch (e) { 1171 | data = false; 1172 | } 1173 | } 1174 | 1175 | if (data) { 1176 | // Binary test 1177 | try { 1178 | dc.binaryType = 'blob'; 1179 | binaryBlob = true; 1180 | } catch (e) { 1181 | } 1182 | 1183 | // Reliable test. 1184 | // Unfortunately Chrome is a bit unreliable about whether or not they 1185 | // support reliable. 1186 | var reliablePC = new RTCPeerConnection(defaultConfig, {}); 1187 | try { 1188 | var reliableDC = reliablePC.createDataChannel('_PEERJSRELIABLETEST', {}); 1189 | sctp = reliableDC.reliable; 1190 | } catch (e) { 1191 | } 1192 | reliablePC.close(); 1193 | } 1194 | 1195 | // FIXME: not really the best check... 1196 | if (audioVideo) { 1197 | audioVideo = !!pc.addStream; 1198 | } 1199 | 1200 | // FIXME: this is not great because in theory it doesn't work for 1201 | // av-only browsers (?). 1202 | if (!onnegotiationneeded && data) { 1203 | // sync default check. 1204 | var negotiationPC = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]}); 1205 | negotiationPC.onnegotiationneeded = function() { 1206 | onnegotiationneeded = true; 1207 | // async check. 1208 | if (util && util.supports) { 1209 | util.supports.onnegotiationneeded = true; 1210 | } 1211 | }; 1212 | var negotiationDC = negotiationPC.createDataChannel('_PEERJSNEGOTIATIONTEST'); 1213 | 1214 | setTimeout(function() { 1215 | negotiationPC.close(); 1216 | }, 1000); 1217 | } 1218 | 1219 | if (pc) { 1220 | pc.close(); 1221 | } 1222 | 1223 | return { 1224 | audioVideo: audioVideo, 1225 | data: data, 1226 | binaryBlob: binaryBlob, 1227 | binary: sctp, // deprecated; sctp implies binary support. 1228 | reliable: sctp, // deprecated; sctp implies reliable data. 1229 | sctp: sctp, 1230 | onnegotiationneeded: onnegotiationneeded 1231 | }; 1232 | }()), 1233 | // 1234 | 1235 | // Ensure alphanumeric ids 1236 | validateId: function(id) { 1237 | // Allow empty ids 1238 | return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id); 1239 | }, 1240 | 1241 | validateKey: function(key) { 1242 | // Allow empty keys 1243 | return !key || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(key); 1244 | }, 1245 | 1246 | 1247 | debug: false, 1248 | 1249 | inherits: function(ctor, superCtor) { 1250 | ctor.super_ = superCtor; 1251 | ctor.prototype = Object.create(superCtor.prototype, { 1252 | constructor: { 1253 | value: ctor, 1254 | enumerable: false, 1255 | writable: true, 1256 | configurable: true 1257 | } 1258 | }); 1259 | }, 1260 | extend: function(dest, source) { 1261 | for(var key in source) { 1262 | if(source.hasOwnProperty(key)) { 1263 | dest[key] = source[key]; 1264 | } 1265 | } 1266 | return dest; 1267 | }, 1268 | pack: BinaryPack.pack, 1269 | unpack: BinaryPack.unpack, 1270 | 1271 | log: function () { 1272 | if (util.debug) { 1273 | var err = false; 1274 | var copy = Array.prototype.slice.call(arguments); 1275 | copy.unshift('PeerJS: '); 1276 | for (var i = 0, l = copy.length; i < l; i++){ 1277 | if (copy[i] instanceof Error) { 1278 | copy[i] = '(' + copy[i].name + ') ' + copy[i].message; 1279 | err = true; 1280 | } 1281 | } 1282 | err ? console.error.apply(console, copy) : console.log.apply(console, copy); 1283 | } 1284 | }, 1285 | 1286 | setZeroTimeout: (function(global) { 1287 | var timeouts = []; 1288 | var messageName = 'zero-timeout-message'; 1289 | 1290 | // Like setTimeout, but only takes a function argument. There's 1291 | // no time argument (always zero) and no arguments (you have to 1292 | // use a closure). 1293 | function setZeroTimeoutPostMessage(fn) { 1294 | timeouts.push(fn); 1295 | global.postMessage(messageName, '*'); 1296 | } 1297 | 1298 | function handleMessage(event) { 1299 | if (event.source == global && event.data == messageName) { 1300 | if (event.stopPropagation) { 1301 | event.stopPropagation(); 1302 | } 1303 | if (timeouts.length) { 1304 | timeouts.shift()(); 1305 | } 1306 | } 1307 | } 1308 | if (global.addEventListener) { 1309 | global.addEventListener('message', handleMessage, true); 1310 | } else if (global.attachEvent) { 1311 | global.attachEvent('onmessage', handleMessage); 1312 | } 1313 | return setZeroTimeoutPostMessage; 1314 | }(this)), 1315 | 1316 | // Binary stuff 1317 | 1318 | // chunks a blob. 1319 | chunk: function(bl) { 1320 | var chunks = []; 1321 | var size = bl.size; 1322 | var start = index = 0; 1323 | var total = Math.ceil(size / util.chunkedMTU); 1324 | while (start < size) { 1325 | var end = Math.min(size, start + util.chunkedMTU); 1326 | var b = bl.slice(start, end); 1327 | 1328 | var chunk = { 1329 | __peerData: dataCount, 1330 | n: index, 1331 | data: b, 1332 | total: total 1333 | }; 1334 | 1335 | chunks.push(chunk); 1336 | 1337 | start = end; 1338 | index += 1; 1339 | } 1340 | dataCount += 1; 1341 | return chunks; 1342 | }, 1343 | 1344 | blobToArrayBuffer: function(blob, cb){ 1345 | var fr = new FileReader(); 1346 | fr.onload = function(evt) { 1347 | cb(evt.target.result); 1348 | }; 1349 | fr.readAsArrayBuffer(blob); 1350 | }, 1351 | blobToBinaryString: function(blob, cb){ 1352 | var fr = new FileReader(); 1353 | fr.onload = function(evt) { 1354 | cb(evt.target.result); 1355 | }; 1356 | fr.readAsBinaryString(blob); 1357 | }, 1358 | binaryStringToArrayBuffer: function(binary) { 1359 | var byteArray = new Uint8Array(binary.length); 1360 | for (var i = 0; i < binary.length; i++) { 1361 | byteArray[i] = binary.charCodeAt(i) & 0xff; 1362 | } 1363 | return byteArray.buffer; 1364 | }, 1365 | randomToken: function () { 1366 | return Math.random().toString(36).substr(2); 1367 | }, 1368 | // 1369 | 1370 | isSecure: function() { 1371 | return location.protocol === 'https:'; 1372 | } 1373 | }; 1374 | 1375 | exports.util = util; 1376 | /** 1377 | * A peer who can initiate connections with other peers. 1378 | */ 1379 | function Peer(id, options) { 1380 | if (!(this instanceof Peer)) return new Peer(id, options); 1381 | EventEmitter.call(this); 1382 | 1383 | // Deal with overloading 1384 | if (id && id.constructor == Object) { 1385 | options = id; 1386 | id = undefined; 1387 | } else if (id) { 1388 | // Ensure id is a string 1389 | id = id.toString(); 1390 | } 1391 | // 1392 | 1393 | // Configurize options 1394 | options = util.extend({ 1395 | debug: 0, // 1: Errors, 2: Warnings, 3: All logs 1396 | host: util.CLOUD_HOST, 1397 | port: util.CLOUD_PORT, 1398 | key: 'peerjs', 1399 | path: '/', 1400 | config: util.defaultConfig 1401 | }, options); 1402 | this.options = options; 1403 | // Detect relative URL host. 1404 | if (options.host === '/') { 1405 | options.host = window.location.hostname; 1406 | } 1407 | // Set path correctly. 1408 | if (options.path[0] !== '/') { 1409 | options.path = '/' + options.path; 1410 | } 1411 | if (options.path[options.path.length - 1] !== '/') { 1412 | options.path += '/'; 1413 | } 1414 | 1415 | // Set whether we use SSL to same as current host 1416 | if (options.secure === undefined && options.host !== util.CLOUD_HOST) { 1417 | options.secure = util.isSecure(); 1418 | } 1419 | // Set a custom log function if present 1420 | if (options.logFunction) { 1421 | util.setLogFunction(options.logFunction); 1422 | } 1423 | util.setLogLevel(options.debug); 1424 | // 1425 | 1426 | // Sanity checks 1427 | // Ensure WebRTC supported 1428 | if (!util.supports.audioVideo && !util.supports.data ) { 1429 | this._delayedAbort('browser-incompatible', 'The current browser does not support WebRTC'); 1430 | return; 1431 | } 1432 | // Ensure alphanumeric id 1433 | if (!util.validateId(id)) { 1434 | this._delayedAbort('invalid-id', 'ID "' + id + '" is invalid'); 1435 | return; 1436 | } 1437 | // Ensure valid key 1438 | if (!util.validateKey(options.key)) { 1439 | this._delayedAbort('invalid-key', 'API KEY "' + options.key + '" is invalid'); 1440 | return; 1441 | } 1442 | // Ensure not using unsecure cloud server on SSL page 1443 | if (options.secure && options.host === '0.peerjs.com') { 1444 | this._delayedAbort('ssl-unavailable', 1445 | 'The cloud server currently does not support HTTPS. Please run your own PeerServer to use HTTPS.'); 1446 | return; 1447 | } 1448 | // 1449 | 1450 | // States. 1451 | this.destroyed = false; // Connections have been killed 1452 | this.disconnected = false; // Connection to PeerServer killed manually but P2P connections still active 1453 | this.open = false; // Sockets and such are not yet open. 1454 | // 1455 | 1456 | // References 1457 | this.connections = {}; // DataConnections for this peer. 1458 | this._lostMessages = {}; // src => [list of messages] 1459 | // 1460 | 1461 | // Initialize the 'socket' (which is actually a mix of XHR streaming and 1462 | // websockets.) 1463 | var self = this; 1464 | this.socket = new Socket(this.options.secure, this.options.host, this.options.port, this.options.path, this.options.key); 1465 | this.socket.on('message', function(data) { 1466 | self._handleMessage(data); 1467 | }); 1468 | this.socket.on('error', function(error) { 1469 | self._abort('socket-error', error); 1470 | }); 1471 | this.socket.on('close', function() { 1472 | if (!self.disconnected) { // If we haven't explicitly disconnected, emit error. 1473 | self._abort('socket-closed', 'Underlying socket is already closed.'); 1474 | } 1475 | }); 1476 | // 1477 | 1478 | // Start the connections 1479 | if (id) { 1480 | this._initialize(id); 1481 | } else { 1482 | this._retrieveId(); 1483 | } 1484 | // 1485 | }; 1486 | 1487 | util.inherits(Peer, EventEmitter); 1488 | 1489 | /** Get a unique ID from the server via XHR. */ 1490 | Peer.prototype._retrieveId = function(cb) { 1491 | var self = this; 1492 | var http = new XMLHttpRequest(); 1493 | var protocol = this.options.secure ? 'https://' : 'http://'; 1494 | var url = protocol + this.options.host + ':' + this.options.port 1495 | + this.options.path + this.options.key + '/id'; 1496 | var queryString = '?ts=' + new Date().getTime() + '' + Math.random(); 1497 | url += queryString; 1498 | 1499 | // If there's no ID we need to wait for one before trying to init socket. 1500 | http.open('get', url, true); 1501 | http.onerror = function(e) { 1502 | util.error('Error retrieving ID', e); 1503 | var pathError = ''; 1504 | if (self.options.path === '/' && self.options.host !== util.CLOUD_HOST) { 1505 | pathError = ' If you passed in a `path` to your self-hosted PeerServer, ' 1506 | + 'you\'ll also need to pass in that same path when creating a new' 1507 | + ' Peer.'; 1508 | } 1509 | self._abort('server-error', 'Could not get an ID from the server.' + pathError); 1510 | } 1511 | http.onreadystatechange = function() { 1512 | if (http.readyState !== 4) { 1513 | return; 1514 | } 1515 | if (http.status !== 200) { 1516 | http.onerror(); 1517 | return; 1518 | } 1519 | self._initialize(http.responseText); 1520 | }; 1521 | http.send(null); 1522 | }; 1523 | 1524 | /** Initialize a connection with the server. */ 1525 | Peer.prototype._initialize = function(id) { 1526 | var self = this; 1527 | this.id = id; 1528 | this.socket.start(this.id); 1529 | } 1530 | 1531 | /** Handles messages from the server. */ 1532 | Peer.prototype._handleMessage = function(message) { 1533 | var type = message.type; 1534 | var payload = message.payload; 1535 | var peer = message.src; 1536 | 1537 | switch (type) { 1538 | case 'OPEN': // The connection to the server is open. 1539 | this.emit('open', this.id); 1540 | this.open = true; 1541 | break; 1542 | case 'ERROR': // Server error. 1543 | this._abort('server-error', payload.msg); 1544 | break; 1545 | case 'ID-TAKEN': // The selected ID is taken. 1546 | this._abort('unavailable-id', 'ID `' + this.id + '` is taken'); 1547 | break; 1548 | case 'INVALID-KEY': // The given API key cannot be found. 1549 | this._abort('invalid-key', 'API KEY "' + this.options.key + '" is invalid'); 1550 | break; 1551 | 1552 | // 1553 | case 'LEAVE': // Another peer has closed its connection to this peer. 1554 | util.log('Received leave message from', peer); 1555 | this._cleanupPeer(peer); 1556 | break; 1557 | 1558 | case 'EXPIRE': // The offer sent to a peer has expired without response. 1559 | this.emit('error', new Error('Could not connect to peer ' + peer)); 1560 | break; 1561 | case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option. 1562 | var connectionId = payload.connectionId; 1563 | var connection = this.getConnection(peer, connectionId); 1564 | 1565 | if (connection) { 1566 | util.warn('Offer received for existing Connection ID:', connectionId); 1567 | //connection.handleMessage(message); 1568 | } else { 1569 | // Create a new connection. 1570 | if (payload.type === 'media') { 1571 | var connection = new MediaConnection(peer, this, { 1572 | connectionId: connectionId, 1573 | _payload: payload, 1574 | metadata: payload.metadata 1575 | }); 1576 | this._addConnection(peer, connection); 1577 | this.emit('call', connection); 1578 | } else if (payload.type === 'data') { 1579 | connection = new DataConnection(peer, this, { 1580 | connectionId: connectionId, 1581 | _payload: payload, 1582 | metadata: payload.metadata, 1583 | label: payload.label, 1584 | serialization: payload.serialization, 1585 | reliable: payload.reliable 1586 | }); 1587 | this._addConnection(peer, connection); 1588 | this.emit('connection', connection); 1589 | } else { 1590 | util.warn('Received malformed connection type:', payload.type); 1591 | return; 1592 | } 1593 | // Find messages. 1594 | var messages = this._getMessages(connectionId); 1595 | for (var i = 0, ii = messages.length; i < ii; i += 1) { 1596 | connection.handleMessage(messages[i]); 1597 | } 1598 | } 1599 | break; 1600 | default: 1601 | if (!payload) { 1602 | util.warn('You received a malformed message from ' + peer + ' of type ' + type); 1603 | return; 1604 | } 1605 | 1606 | var id = payload.connectionId; 1607 | var connection = this.getConnection(peer, id); 1608 | 1609 | if (connection && connection.pc) { 1610 | // Pass it on. 1611 | connection.handleMessage(message); 1612 | } else if (id) { 1613 | // Store for possible later use 1614 | this._storeMessage(id, message); 1615 | } else { 1616 | util.warn('You received an unrecognized message:', message); 1617 | } 1618 | break; 1619 | } 1620 | } 1621 | 1622 | /** Stores messages without a set up connection, to be claimed later. */ 1623 | Peer.prototype._storeMessage = function(connectionId, message) { 1624 | if (!this._lostMessages[connectionId]) { 1625 | this._lostMessages[connectionId] = []; 1626 | } 1627 | this._lostMessages[connectionId].push(message); 1628 | } 1629 | 1630 | /** Retrieve messages from lost message store */ 1631 | Peer.prototype._getMessages = function(connectionId) { 1632 | var messages = this._lostMessages[connectionId]; 1633 | if (messages) { 1634 | delete this._lostMessages[connectionId]; 1635 | return messages; 1636 | } else { 1637 | return []; 1638 | } 1639 | } 1640 | 1641 | /** 1642 | * Returns a DataConnection to the specified peer. See documentation for a 1643 | * complete list of options. 1644 | */ 1645 | Peer.prototype.connect = function(peer, options) { 1646 | if (this.disconnected) { 1647 | util.warn('You cannot connect to a new Peer because you called ' 1648 | + '.disconnect() on this Peer and ended your connection with the' 1649 | + ' server. You can create a new Peer to reconnect.'); 1650 | this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.')); 1651 | return; 1652 | } 1653 | var connection = new DataConnection(peer, this, options); 1654 | this._addConnection(peer, connection); 1655 | return connection; 1656 | } 1657 | 1658 | /** 1659 | * Returns a MediaConnection to the specified peer. See documentation for a 1660 | * complete list of options. 1661 | */ 1662 | Peer.prototype.call = function(peer, stream, options) { 1663 | if (this.disconnected) { 1664 | util.warn('You cannot connect to a new Peer because you called ' 1665 | + '.disconnect() on this Peer and ended your connection with the' 1666 | + ' server. You can create a new Peer to reconnect.'); 1667 | this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.')); 1668 | return; 1669 | } 1670 | if (!stream) { 1671 | util.error('To call a peer, you must provide a stream from your browser\'s `getUserMedia`.'); 1672 | return; 1673 | } 1674 | options = options || {}; 1675 | options._stream = stream; 1676 | var call = new MediaConnection(peer, this, options); 1677 | this._addConnection(peer, call); 1678 | return call; 1679 | } 1680 | 1681 | /** Add a data/media connection to this peer. */ 1682 | Peer.prototype._addConnection = function(peer, connection) { 1683 | if (!this.connections[peer]) { 1684 | this.connections[peer] = []; 1685 | } 1686 | this.connections[peer].push(connection); 1687 | } 1688 | 1689 | /** Retrieve a data/media connection for this peer. */ 1690 | Peer.prototype.getConnection = function(peer, id) { 1691 | var connections = this.connections[peer]; 1692 | if (!connections) { 1693 | return null; 1694 | } 1695 | for (var i = 0, ii = connections.length; i < ii; i++) { 1696 | if (connections[i].id === id) { 1697 | return connections[i]; 1698 | } 1699 | } 1700 | return null; 1701 | } 1702 | 1703 | Peer.prototype._delayedAbort = function(type, message) { 1704 | var self = this; 1705 | util.setZeroTimeout(function(){ 1706 | self._abort(type, message); 1707 | }); 1708 | } 1709 | 1710 | /** Destroys the Peer and emits an error message. */ 1711 | Peer.prototype._abort = function(type, message) { 1712 | util.error('Aborting. Error:', message); 1713 | var err = new Error(message); 1714 | err.type = type; 1715 | this.destroy(); 1716 | this.emit('error', err); 1717 | }; 1718 | 1719 | /** 1720 | * Destroys the Peer: closes all active connections as well as the connection 1721 | * to the server. 1722 | * Warning: The peer can no longer create or accept connections after being 1723 | * destroyed. 1724 | */ 1725 | Peer.prototype.destroy = function() { 1726 | if (!this.destroyed) { 1727 | this._cleanup(); 1728 | this.disconnect(); 1729 | this.destroyed = true; 1730 | } 1731 | } 1732 | 1733 | 1734 | /** Disconnects every connection on this peer. */ 1735 | Peer.prototype._cleanup = function() { 1736 | if (this.connections) { 1737 | var peers = Object.keys(this.connections); 1738 | for (var i = 0, ii = peers.length; i < ii; i++) { 1739 | this._cleanupPeer(peers[i]); 1740 | } 1741 | } 1742 | this.emit('close'); 1743 | } 1744 | 1745 | /** Closes all connections to this peer. */ 1746 | Peer.prototype._cleanupPeer = function(peer) { 1747 | var connections = this.connections[peer]; 1748 | for (var j = 0, jj = connections.length; j < jj; j += 1) { 1749 | connections[j].close(); 1750 | } 1751 | } 1752 | 1753 | /** 1754 | * Disconnects the Peer's connection to the PeerServer. Does not close any 1755 | * active connections. 1756 | * Warning: The peer can no longer create or accept connections after being 1757 | * disconnected. It also cannot reconnect to the server. 1758 | */ 1759 | Peer.prototype.disconnect = function() { 1760 | var self = this; 1761 | util.setZeroTimeout(function(){ 1762 | if (!self.disconnected) { 1763 | self.disconnected = true; 1764 | self.open = false; 1765 | if (self.socket) { 1766 | self.socket.close(); 1767 | } 1768 | self.id = null; 1769 | } 1770 | }); 1771 | } 1772 | 1773 | /** 1774 | * Get a list of available peer IDs. If you're running your own server, you'll 1775 | * want to set allow_discovery: true in the PeerServer options. If you're using 1776 | * the cloud server, email team@peerjs.com to get the functionality enabled for 1777 | * your key. 1778 | */ 1779 | Peer.prototype.listAllPeers = function(cb) { 1780 | cb = cb || function() {}; 1781 | var self = this; 1782 | var http = new XMLHttpRequest(); 1783 | var protocol = this.options.secure ? 'https://' : 'http://'; 1784 | var url = protocol + this.options.host + ':' + this.options.port 1785 | + this.options.path + this.options.key + '/peers'; 1786 | var queryString = '?ts=' + new Date().getTime() + '' + Math.random(); 1787 | url += queryString; 1788 | 1789 | // If there's no ID we need to wait for one before trying to init socket. 1790 | http.open('get', url, true); 1791 | http.onerror = function(e) { 1792 | self._abort('server-error', 'Could not get peers from the server.'); 1793 | cb([]); 1794 | } 1795 | http.onreadystatechange = function() { 1796 | if (http.readyState !== 4) { 1797 | return; 1798 | } 1799 | if (http.status === 401) { 1800 | var helpfulError = ''; 1801 | if (self.options.host !== util.CLOUD_HOST) { 1802 | helpfulError = 'It looks like you\'re using the cloud server. You can email ' 1803 | + 'team@peerjs.com to enable peer listing for your API key.'; 1804 | } else { 1805 | helpfulError = 'You need to enable `allow_discovery` on your self-hosted' 1806 | + ' PeerServer to use this feature.'; 1807 | } 1808 | throw new Error('It doesn\'t look like you have permission to list peers IDs. ' + helpfulError); 1809 | cb([]); 1810 | } else if (http.status !== 200) { 1811 | cb([]); 1812 | } else { 1813 | cb(JSON.parse(http.responseText)); 1814 | } 1815 | }; 1816 | http.send(null); 1817 | } 1818 | 1819 | exports.Peer = Peer; 1820 | /** 1821 | * Wraps a DataChannel between two Peers. 1822 | */ 1823 | function DataConnection(peer, provider, options) { 1824 | if (!(this instanceof DataConnection)) return new DataConnection(peer, provider, options); 1825 | EventEmitter.call(this); 1826 | 1827 | this.options = util.extend({ 1828 | serialization: 'binary', 1829 | reliable: false 1830 | }, options); 1831 | 1832 | // Connection is not open yet. 1833 | this.open = false; 1834 | this.type = 'data'; 1835 | this.peer = peer; 1836 | this.provider = provider; 1837 | 1838 | this.id = this.options.connectionId || DataConnection._idPrefix + util.randomToken(); 1839 | 1840 | this.label = this.options.label || this.id; 1841 | this.metadata = this.options.metadata; 1842 | this.serialization = this.options.serialization; 1843 | this.reliable = this.options.reliable; 1844 | 1845 | // Data channel buffering. 1846 | this._buffer = []; 1847 | this._buffering = false; 1848 | this.bufferSize = 0; 1849 | 1850 | // For storing large data. 1851 | this._chunkedData = {}; 1852 | 1853 | if (this.options._payload) { 1854 | this._peerBrowser = this.options._payload.browser; 1855 | } 1856 | 1857 | Negotiator.startConnection( 1858 | this, 1859 | this.options._payload || { 1860 | originator: true 1861 | } 1862 | ); 1863 | } 1864 | 1865 | util.inherits(DataConnection, EventEmitter); 1866 | 1867 | DataConnection._idPrefix = 'dc_'; 1868 | 1869 | /** Called by the Negotiator when the DataChannel is ready. */ 1870 | DataConnection.prototype.initialize = function(dc) { 1871 | this._dc = this.dataChannel = dc; 1872 | this._configureDataChannel(); 1873 | } 1874 | 1875 | DataConnection.prototype._configureDataChannel = function() { 1876 | var self = this; 1877 | if (util.supports.sctp) { 1878 | this._dc.binaryType = 'arraybuffer'; 1879 | } 1880 | this._dc.onopen = function() { 1881 | util.log('Data channel connection success'); 1882 | self.open = true; 1883 | self.emit('open'); 1884 | } 1885 | 1886 | // Use the Reliable shim for non Firefox browsers 1887 | if (!util.supports.sctp && this.reliable) { 1888 | this._reliable = new Reliable(this._dc, util.debug); 1889 | } 1890 | 1891 | if (this._reliable) { 1892 | this._reliable.onmessage = function(msg) { 1893 | self.emit('data', msg); 1894 | }; 1895 | } else { 1896 | this._dc.onmessage = function(e) { 1897 | self._handleDataMessage(e); 1898 | }; 1899 | } 1900 | this._dc.onclose = function(e) { 1901 | util.log('DataChannel closed for:', self.peer); 1902 | self.close(); 1903 | }; 1904 | } 1905 | 1906 | // Handles a DataChannel message. 1907 | DataConnection.prototype._handleDataMessage = function(e) { 1908 | var self = this; 1909 | var data = e.data; 1910 | var datatype = data.constructor; 1911 | if (this.serialization === 'binary' || this.serialization === 'binary-utf8') { 1912 | if (datatype === Blob) { 1913 | // Datatype should never be blob 1914 | util.blobToArrayBuffer(data, function(ab) { 1915 | data = util.unpack(ab); 1916 | self.emit('data', data); 1917 | }); 1918 | return; 1919 | } else if (datatype === ArrayBuffer) { 1920 | data = util.unpack(data); 1921 | } else if (datatype === String) { 1922 | // String fallback for binary data for browsers that don't support binary yet 1923 | var ab = util.binaryStringToArrayBuffer(data); 1924 | data = util.unpack(ab); 1925 | } 1926 | } else if (this.serialization === 'json') { 1927 | data = JSON.parse(data); 1928 | } 1929 | 1930 | // Check if we've chunked--if so, piece things back together. 1931 | // We're guaranteed that this isn't 0. 1932 | if (data.__peerData) { 1933 | var id = data.__peerData; 1934 | var chunkInfo = this._chunkedData[id] || {data: [], count: 0, total: data.total}; 1935 | 1936 | chunkInfo.data[data.n] = data.data; 1937 | chunkInfo.count += 1; 1938 | 1939 | if (chunkInfo.total === chunkInfo.count) { 1940 | // We've received all the chunks--time to construct the complete data. 1941 | data = new Blob(chunkInfo.data); 1942 | this._handleDataMessage({data: data}); 1943 | 1944 | // We can also just delete the chunks now. 1945 | delete this._chunkedData[id]; 1946 | } 1947 | 1948 | this._chunkedData[id] = chunkInfo; 1949 | return; 1950 | } 1951 | 1952 | this.emit('data', data); 1953 | } 1954 | 1955 | /** 1956 | * Exposed functionality for users. 1957 | */ 1958 | 1959 | /** Allows user to close connection. */ 1960 | DataConnection.prototype.close = function() { 1961 | if (!this.open) { 1962 | return; 1963 | } 1964 | this.open = false; 1965 | Negotiator.cleanup(this); 1966 | this.emit('close'); 1967 | } 1968 | 1969 | /** Allows user to send data. */ 1970 | DataConnection.prototype.send = function(data, chunked) { 1971 | if (!this.open) { 1972 | this.emit('error', new Error('Connection is not open. You should listen for the `open` event before sending messages.')); 1973 | return; 1974 | } 1975 | if (this._reliable) { 1976 | // Note: reliable shim sending will make it so that you cannot customize 1977 | // serialization. 1978 | this._reliable.send(data); 1979 | return; 1980 | } 1981 | var self = this; 1982 | if (this.serialization === 'json') { 1983 | this._bufferedSend(JSON.stringify(data)); 1984 | } else if (this.serialization === 'binary' || this.serialization === 'binary-utf8') { 1985 | var blob = util.pack(data); 1986 | 1987 | // For Chrome-Firefox interoperability, we need to make Firefox "chunk" 1988 | // the data it sends out. 1989 | var needsChunking = util.chunkedBrowsers[this._peerBrowser] || util.chunkedBrowsers[util.browser]; 1990 | if (needsChunking && !chunked && blob.size > util.chunkedMTU) { 1991 | this._sendChunks(blob); 1992 | return; 1993 | } 1994 | 1995 | // DataChannel currently only supports strings. 1996 | if (!util.supports.sctp) { 1997 | util.blobToBinaryString(blob, function(str) { 1998 | self._bufferedSend(str); 1999 | }); 2000 | } else if (!util.supports.binaryBlob) { 2001 | // We only do this if we really need to (e.g. blobs are not supported), 2002 | // because this conversion is costly. 2003 | util.blobToArrayBuffer(blob, function(ab) { 2004 | self._bufferedSend(ab); 2005 | }); 2006 | } else { 2007 | this._bufferedSend(blob); 2008 | } 2009 | } else { 2010 | this._bufferedSend(data); 2011 | } 2012 | } 2013 | 2014 | DataConnection.prototype._bufferedSend = function(msg) { 2015 | if (this._buffering || !this._trySend(msg)) { 2016 | this._buffer.push(msg); 2017 | this.bufferSize = this._buffer.length; 2018 | } 2019 | } 2020 | 2021 | // Returns true if the send succeeds. 2022 | DataConnection.prototype._trySend = function(msg) { 2023 | try { 2024 | this._dc.send(msg); 2025 | } catch (e) { 2026 | this._buffering = true; 2027 | 2028 | var self = this; 2029 | setTimeout(function() { 2030 | // Try again. 2031 | self._buffering = false; 2032 | self._tryBuffer(); 2033 | }, 100); 2034 | return false; 2035 | } 2036 | return true; 2037 | } 2038 | 2039 | // Try to send the first message in the buffer. 2040 | DataConnection.prototype._tryBuffer = function() { 2041 | if (this._buffer.length === 0) { 2042 | return; 2043 | } 2044 | 2045 | var msg = this._buffer[0]; 2046 | 2047 | if (this._trySend(msg)) { 2048 | this._buffer.shift(); 2049 | this.bufferSize = this._buffer.length; 2050 | this._tryBuffer(); 2051 | } 2052 | } 2053 | 2054 | DataConnection.prototype._sendChunks = function(blob) { 2055 | var blobs = util.chunk(blob); 2056 | for (var i = 0, ii = blobs.length; i < ii; i += 1) { 2057 | var blob = blobs[i]; 2058 | this.send(blob, true); 2059 | } 2060 | } 2061 | 2062 | DataConnection.prototype.handleMessage = function(message) { 2063 | var payload = message.payload; 2064 | 2065 | switch (message.type) { 2066 | case 'ANSWER': 2067 | this._peerBrowser = payload.browser; 2068 | 2069 | // Forward to negotiator 2070 | Negotiator.handleSDP(message.type, this, payload.sdp); 2071 | break; 2072 | case 'CANDIDATE': 2073 | Negotiator.handleCandidate(this, payload.candidate); 2074 | break; 2075 | default: 2076 | util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer); 2077 | break; 2078 | } 2079 | } 2080 | /** 2081 | * Wraps the streaming interface between two Peers. 2082 | */ 2083 | function MediaConnection(peer, provider, options) { 2084 | if (!(this instanceof MediaConnection)) return new MediaConnection(peer, provider, options); 2085 | EventEmitter.call(this); 2086 | 2087 | this.options = util.extend({}, options); 2088 | 2089 | this.open = false; 2090 | this.type = 'media'; 2091 | this.peer = peer; 2092 | this.provider = provider; 2093 | this.metadata = this.options.metadata; 2094 | this.localStream = this.options._stream; 2095 | 2096 | this.id = this.options.connectionId || MediaConnection._idPrefix + util.randomToken(); 2097 | if (this.localStream) { 2098 | Negotiator.startConnection( 2099 | this, 2100 | {_stream: this.localStream, originator: true} 2101 | ); 2102 | } 2103 | }; 2104 | 2105 | util.inherits(MediaConnection, EventEmitter); 2106 | 2107 | MediaConnection._idPrefix = 'mc_'; 2108 | 2109 | MediaConnection.prototype.addStream = function(remoteStream) { 2110 | util.log('Receiving stream', remoteStream); 2111 | 2112 | this.remoteStream = remoteStream; 2113 | this.emit('stream', remoteStream); // Should we call this `open`? 2114 | 2115 | }; 2116 | 2117 | MediaConnection.prototype.handleMessage = function(message) { 2118 | var payload = message.payload; 2119 | 2120 | switch (message.type) { 2121 | case 'ANSWER': 2122 | // Forward to negotiator 2123 | Negotiator.handleSDP(message.type, this, payload.sdp); 2124 | this.open = true; 2125 | break; 2126 | case 'CANDIDATE': 2127 | Negotiator.handleCandidate(this, payload.candidate); 2128 | break; 2129 | default: 2130 | util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer); 2131 | break; 2132 | } 2133 | } 2134 | 2135 | MediaConnection.prototype.answer = function(stream) { 2136 | if (this.localStream) { 2137 | util.warn('Local stream already exists on this MediaConnection. Are you answering a call twice?'); 2138 | return; 2139 | } 2140 | 2141 | this.options._payload._stream = stream; 2142 | 2143 | this.localStream = stream; 2144 | Negotiator.startConnection( 2145 | this, 2146 | this.options._payload 2147 | ) 2148 | // Retrieve lost messages stored because PeerConnection not set up. 2149 | var messages = this.provider._getMessages(this.id); 2150 | for (var i = 0, ii = messages.length; i < ii; i += 1) { 2151 | this.handleMessage(messages[i]); 2152 | } 2153 | this.open = true; 2154 | }; 2155 | 2156 | /** 2157 | * Exposed functionality for users. 2158 | */ 2159 | 2160 | /** Allows user to close connection. */ 2161 | MediaConnection.prototype.close = function() { 2162 | if (!this.open) { 2163 | return; 2164 | } 2165 | this.open = false; 2166 | Negotiator.cleanup(this); 2167 | this.emit('close') 2168 | }; 2169 | /** 2170 | * Manages all negotiations between Peers. 2171 | */ 2172 | var Negotiator = { 2173 | pcs: { 2174 | data: {}, 2175 | media: {} 2176 | }, // type => {peerId: {pc_id: pc}}. 2177 | //providers: {}, // provider's id => providers (there may be multiple providers/client. 2178 | queue: [] // connections that are delayed due to a PC being in use. 2179 | } 2180 | 2181 | Negotiator._idPrefix = 'pc_'; 2182 | 2183 | /** Returns a PeerConnection object set up correctly (for data, media). */ 2184 | Negotiator.startConnection = function(connection, options) { 2185 | var pc = Negotiator._getPeerConnection(connection, options); 2186 | 2187 | if (connection.type === 'media' && options._stream) { 2188 | // Add the stream. 2189 | pc.addStream(options._stream); 2190 | } 2191 | 2192 | // Set the connection's PC. 2193 | connection.pc = connection.peerConnection = pc; 2194 | // What do we need to do now? 2195 | if (options.originator) { 2196 | if (connection.type === 'data') { 2197 | // Create the datachannel. 2198 | var config = {}; 2199 | // Dropping reliable:false support, since it seems to be crashing 2200 | // Chrome. 2201 | /*if (util.supports.sctp && !options.reliable) { 2202 | // If we have canonical reliable support... 2203 | config = {maxRetransmits: 0}; 2204 | }*/ 2205 | // Fallback to ensure older browsers don't crash. 2206 | if (!util.supports.sctp) { 2207 | config = {reliable: options.reliable}; 2208 | } 2209 | var dc = pc.createDataChannel(connection.label, config); 2210 | connection.initialize(dc); 2211 | } 2212 | 2213 | if (!util.supports.onnegotiationneeded) { 2214 | Negotiator._makeOffer(connection); 2215 | } 2216 | } else { 2217 | Negotiator.handleSDP('OFFER', connection, options.sdp); 2218 | } 2219 | } 2220 | 2221 | Negotiator._getPeerConnection = function(connection, options) { 2222 | if (!Negotiator.pcs[connection.type]) { 2223 | util.error(connection.type + ' is not a valid connection type. Maybe you overrode the `type` property somewhere.'); 2224 | } 2225 | 2226 | if (!Negotiator.pcs[connection.type][connection.peer]) { 2227 | Negotiator.pcs[connection.type][connection.peer] = {}; 2228 | } 2229 | var peerConnections = Negotiator.pcs[connection.type][connection.peer]; 2230 | 2231 | var pc; 2232 | // Not multiplexing while FF and Chrome have not-great support for it. 2233 | /*if (options.multiplex) { 2234 | ids = Object.keys(peerConnections); 2235 | for (var i = 0, ii = ids.length; i < ii; i += 1) { 2236 | pc = peerConnections[ids[i]]; 2237 | if (pc.signalingState === 'stable') { 2238 | break; // We can go ahead and use this PC. 2239 | } 2240 | } 2241 | } else */ 2242 | if (options.pc) { // Simplest case: PC id already provided for us. 2243 | pc = Negotiator.pcs[connection.type][connection.peer][options.pc]; 2244 | } 2245 | 2246 | if (!pc || pc.signalingState !== 'stable') { 2247 | pc = Negotiator._startPeerConnection(connection); 2248 | } 2249 | return pc; 2250 | } 2251 | 2252 | /* 2253 | Negotiator._addProvider = function(provider) { 2254 | if ((!provider.id && !provider.disconnected) || !provider.socket.open) { 2255 | // Wait for provider to obtain an ID. 2256 | provider.on('open', function(id) { 2257 | Negotiator._addProvider(provider); 2258 | }); 2259 | } else { 2260 | Negotiator.providers[provider.id] = provider; 2261 | } 2262 | }*/ 2263 | 2264 | 2265 | /** Start a PC. */ 2266 | Negotiator._startPeerConnection = function(connection) { 2267 | util.log('Creating RTCPeerConnection.'); 2268 | 2269 | var id = Negotiator._idPrefix + util.randomToken(); 2270 | var optional = {}; 2271 | 2272 | if (connection.type === 'data' && !util.supports.sctp) { 2273 | optional = {optional: [{RtpDataChannels: true}]}; 2274 | } else if (connection.type === 'media') { 2275 | // Interop req for chrome. 2276 | optional = {optional: [{DtlsSrtpKeyAgreement: true}]}; 2277 | } 2278 | 2279 | var pc = new RTCPeerConnection(connection.provider.options.config, optional); 2280 | Negotiator.pcs[connection.type][connection.peer][id] = pc; 2281 | 2282 | Negotiator._setupListeners(connection, pc, id); 2283 | 2284 | return pc; 2285 | } 2286 | 2287 | /** Set up various WebRTC listeners. */ 2288 | Negotiator._setupListeners = function(connection, pc, pc_id) { 2289 | var peerId = connection.peer; 2290 | var connectionId = connection.id; 2291 | var provider = connection.provider; 2292 | 2293 | // ICE CANDIDATES. 2294 | util.log('Listening for ICE candidates.'); 2295 | pc.onicecandidate = function(evt) { 2296 | if (evt.candidate) { 2297 | util.log('Received ICE candidates for:', connection.peer); 2298 | provider.socket.send({ 2299 | type: 'CANDIDATE', 2300 | payload: { 2301 | candidate: evt.candidate, 2302 | type: connection.type, 2303 | connectionId: connection.id 2304 | }, 2305 | dst: peerId 2306 | }); 2307 | } 2308 | }; 2309 | 2310 | pc.oniceconnectionstatechange = function() { 2311 | switch (pc.iceConnectionState) { 2312 | case 'disconnected': 2313 | case 'failed': 2314 | util.log('iceConnectionState is disconnected, closing connections to ' + peerId); 2315 | connection.close(); 2316 | break; 2317 | case 'completed': 2318 | pc.onicecandidate = util.noop; 2319 | break; 2320 | } 2321 | }; 2322 | 2323 | // Fallback for older Chrome impls. 2324 | pc.onicechange = pc.oniceconnectionstatechange; 2325 | 2326 | // ONNEGOTIATIONNEEDED (Chrome) 2327 | util.log('Listening for `negotiationneeded`'); 2328 | pc.onnegotiationneeded = function() { 2329 | util.log('`negotiationneeded` triggered'); 2330 | if (pc.signalingState == 'stable') { 2331 | Negotiator._makeOffer(connection); 2332 | } else { 2333 | util.log('onnegotiationneeded triggered when not stable. Is another connection being established?'); 2334 | } 2335 | }; 2336 | 2337 | // DATACONNECTION. 2338 | util.log('Listening for data channel'); 2339 | // Fired between offer and answer, so options should already be saved 2340 | // in the options hash. 2341 | pc.ondatachannel = function(evt) { 2342 | util.log('Received data channel'); 2343 | var dc = evt.channel; 2344 | var connection = provider.getConnection(peerId, connectionId); 2345 | connection.initialize(dc); 2346 | }; 2347 | 2348 | // MEDIACONNECTION. 2349 | util.log('Listening for remote stream'); 2350 | pc.onaddstream = function(evt) { 2351 | util.log('Received remote stream'); 2352 | var stream = evt.stream; 2353 | provider.getConnection(peerId, connectionId).addStream(stream); 2354 | }; 2355 | } 2356 | 2357 | Negotiator.cleanup = function(connection) { 2358 | util.log('Cleaning up PeerConnection to ' + connection.peer); 2359 | 2360 | var pc = connection.pc; 2361 | 2362 | if (!!pc && (pc.readyState !== 'closed' || pc.signalingState !== 'closed')) { 2363 | pc.close(); 2364 | connection.pc = null; 2365 | } 2366 | } 2367 | 2368 | Negotiator._makeOffer = function(connection) { 2369 | var pc = connection.pc; 2370 | pc.createOffer(function(offer) { 2371 | util.log('Created offer.'); 2372 | 2373 | if (!util.supports.sctp && connection.type === 'data' && connection.reliable) { 2374 | offer.sdp = Reliable.higherBandwidthSDP(offer.sdp); 2375 | } 2376 | 2377 | pc.setLocalDescription(offer, function() { 2378 | util.log('Set localDescription: offer', 'for:', connection.peer); 2379 | connection.provider.socket.send({ 2380 | type: 'OFFER', 2381 | payload: { 2382 | sdp: offer, 2383 | type: connection.type, 2384 | label: connection.label, 2385 | connectionId: connection.id, 2386 | reliable: connection.reliable, 2387 | serialization: connection.serialization, 2388 | metadata: connection.metadata, 2389 | browser: util.browser 2390 | }, 2391 | dst: connection.peer 2392 | }); 2393 | }, function(err) { 2394 | connection.provider.emit('error', err); 2395 | util.log('Failed to setLocalDescription, ', err); 2396 | }); 2397 | }, function(err) { 2398 | connection.provider.emit('error', err); 2399 | util.log('Failed to createOffer, ', err); 2400 | }, connection.options.constraints); 2401 | } 2402 | 2403 | Negotiator._makeAnswer = function(connection) { 2404 | var pc = connection.pc; 2405 | 2406 | pc.createAnswer(function(answer) { 2407 | util.log('Created answer.'); 2408 | 2409 | if (!util.supports.sctp && connection.type === 'data' && connection.reliable) { 2410 | answer.sdp = Reliable.higherBandwidthSDP(answer.sdp); 2411 | } 2412 | 2413 | pc.setLocalDescription(answer, function() { 2414 | util.log('Set localDescription: answer', 'for:', connection.peer); 2415 | connection.provider.socket.send({ 2416 | type: 'ANSWER', 2417 | payload: { 2418 | sdp: answer, 2419 | type: connection.type, 2420 | connectionId: connection.id, 2421 | browser: util.browser 2422 | }, 2423 | dst: connection.peer 2424 | }); 2425 | }, function(err) { 2426 | connection.provider.emit('error', err); 2427 | util.log('Failed to setLocalDescription, ', err); 2428 | }); 2429 | }, function(err) { 2430 | connection.provider.emit('error', err); 2431 | util.log('Failed to create answer, ', err); 2432 | }); 2433 | } 2434 | 2435 | /** Handle an SDP. */ 2436 | Negotiator.handleSDP = function(type, connection, sdp) { 2437 | sdp = new RTCSessionDescription(sdp); 2438 | var pc = connection.pc; 2439 | 2440 | util.log('Setting remote description', sdp); 2441 | pc.setRemoteDescription(sdp, function() { 2442 | util.log('Set remoteDescription:', type, 'for:', connection.peer); 2443 | 2444 | if (type === 'OFFER') { 2445 | Negotiator._makeAnswer(connection); 2446 | } 2447 | }, function(err) { 2448 | connection.provider.emit('error', err); 2449 | util.log('Failed to setRemoteDescription, ', err); 2450 | }); 2451 | } 2452 | 2453 | /** Handle a candidate. */ 2454 | Negotiator.handleCandidate = function(connection, ice) { 2455 | var candidate = ice.candidate; 2456 | var sdpMLineIndex = ice.sdpMLineIndex; 2457 | connection.pc.addIceCandidate(new RTCIceCandidate({ 2458 | sdpMLineIndex: sdpMLineIndex, 2459 | candidate: candidate 2460 | })); 2461 | util.log('Added ICE candidate for:', connection.peer); 2462 | } 2463 | /** 2464 | * An abstraction on top of WebSockets and XHR streaming to provide fastest 2465 | * possible connection for peers. 2466 | */ 2467 | function Socket(secure, host, port, path, key) { 2468 | if (!(this instanceof Socket)) return new Socket(secure, host, port, path, key); 2469 | 2470 | EventEmitter.call(this); 2471 | 2472 | // Disconnected manually. 2473 | this.disconnected = false; 2474 | this._queue = []; 2475 | 2476 | var httpProtocol = secure ? 'https://' : 'http://'; 2477 | var wsProtocol = secure ? 'wss://' : 'ws://'; 2478 | this._httpUrl = httpProtocol + host + ':' + port + path + key; 2479 | this._wsUrl = wsProtocol + host + ':' + port + path + 'peerjs?key=' + key; 2480 | } 2481 | 2482 | util.inherits(Socket, EventEmitter); 2483 | 2484 | 2485 | /** Check in with ID or get one from server. */ 2486 | Socket.prototype.start = function(id) { 2487 | this.id = id; 2488 | 2489 | var token = util.randomToken(); 2490 | this._httpUrl += '/' + id + '/' + token; 2491 | this._wsUrl += '&id='+id+'&token='+token; 2492 | 2493 | this._startXhrStream(); 2494 | this._startWebSocket(); 2495 | } 2496 | 2497 | 2498 | /** Start up websocket communications. */ 2499 | Socket.prototype._startWebSocket = function(id) { 2500 | var self = this; 2501 | 2502 | if (this._socket) { 2503 | return; 2504 | } 2505 | 2506 | this._socket = new WebSocket(this._wsUrl); 2507 | 2508 | this._socket.onmessage = function(event) { 2509 | var data; 2510 | try { 2511 | data = JSON.parse(event.data); 2512 | } catch(e) { 2513 | util.log('Invalid server message', event.data); 2514 | return; 2515 | } 2516 | self.emit('message', data); 2517 | }; 2518 | 2519 | // Take care of the queue of connections if necessary and make sure Peer knows 2520 | // socket is open. 2521 | this._socket.onopen = function() { 2522 | if (self._timeout) { 2523 | clearTimeout(self._timeout); 2524 | setTimeout(function(){ 2525 | self._http.abort(); 2526 | self._http = null; 2527 | }, 5000); 2528 | } 2529 | self._sendQueuedMessages(); 2530 | util.log('Socket open'); 2531 | }; 2532 | } 2533 | 2534 | /** Start XHR streaming. */ 2535 | Socket.prototype._startXhrStream = function(n) { 2536 | try { 2537 | var self = this; 2538 | this._http = new XMLHttpRequest(); 2539 | this._http._index = 1; 2540 | this._http._streamIndex = n || 0; 2541 | this._http.open('post', this._httpUrl + '/id?i=' + this._http._streamIndex, true); 2542 | this._http.onreadystatechange = function() { 2543 | if (this.readyState == 2 && this.old) { 2544 | this.old.abort(); 2545 | delete this.old; 2546 | } 2547 | if (this.readyState > 2 && this.status == 200 && this.responseText) { 2548 | self._handleStream(this); 2549 | } 2550 | }; 2551 | this._http.send(null); 2552 | this._setHTTPTimeout(); 2553 | } catch(e) { 2554 | util.log('XMLHttpRequest not available; defaulting to WebSockets'); 2555 | } 2556 | } 2557 | 2558 | 2559 | /** Handles onreadystatechange response as a stream. */ 2560 | Socket.prototype._handleStream = function(http) { 2561 | // 3 and 4 are loading/done state. All others are not relevant. 2562 | var messages = http.responseText.split('\n'); 2563 | 2564 | // Check to see if anything needs to be processed on buffer. 2565 | if (http._buffer) { 2566 | while (http._buffer.length > 0) { 2567 | var index = http._buffer.shift(); 2568 | var bufferedMessage = messages[index]; 2569 | try { 2570 | bufferedMessage = JSON.parse(bufferedMessage); 2571 | } catch(e) { 2572 | http._buffer.shift(index); 2573 | break; 2574 | } 2575 | this.emit('message', bufferedMessage); 2576 | } 2577 | } 2578 | 2579 | var message = messages[http._index]; 2580 | if (message) { 2581 | http._index += 1; 2582 | // Buffering--this message is incomplete and we'll get to it next time. 2583 | // This checks if the httpResponse ended in a `\n`, in which case the last 2584 | // element of messages should be the empty string. 2585 | if (http._index === messages.length) { 2586 | if (!http._buffer) { 2587 | http._buffer = []; 2588 | } 2589 | http._buffer.push(http._index - 1); 2590 | } else { 2591 | try { 2592 | message = JSON.parse(message); 2593 | } catch(e) { 2594 | util.log('Invalid server message', message); 2595 | return; 2596 | } 2597 | this.emit('message', message); 2598 | } 2599 | } 2600 | } 2601 | 2602 | Socket.prototype._setHTTPTimeout = function() { 2603 | var self = this; 2604 | this._timeout = setTimeout(function() { 2605 | var old = self._http; 2606 | if (!self._wsOpen()) { 2607 | self._startXhrStream(old._streamIndex + 1); 2608 | self._http.old = old; 2609 | } else { 2610 | old.abort(); 2611 | } 2612 | }, 25000); 2613 | } 2614 | 2615 | /** Is the websocket currently open? */ 2616 | Socket.prototype._wsOpen = function() { 2617 | return this._socket && this._socket.readyState == 1; 2618 | } 2619 | 2620 | /** Send queued messages. */ 2621 | Socket.prototype._sendQueuedMessages = function() { 2622 | for (var i = 0, ii = this._queue.length; i < ii; i += 1) { 2623 | this.send(this._queue[i]); 2624 | } 2625 | } 2626 | 2627 | /** Exposed send for DC & Peer. */ 2628 | Socket.prototype.send = function(data) { 2629 | if (this.disconnected) { 2630 | return; 2631 | } 2632 | 2633 | // If we didn't get an ID yet, we can't yet send anything so we should queue 2634 | // up these messages. 2635 | if (!this.id) { 2636 | this._queue.push(data); 2637 | return; 2638 | } 2639 | 2640 | if (!data.type) { 2641 | this.emit('error', 'Invalid message'); 2642 | return; 2643 | } 2644 | 2645 | var message = JSON.stringify(data); 2646 | if (this._wsOpen()) { 2647 | this._socket.send(message); 2648 | } else { 2649 | var http = new XMLHttpRequest(); 2650 | var url = this._httpUrl + '/' + data.type.toLowerCase(); 2651 | http.open('post', url, true); 2652 | http.setRequestHeader('Content-Type', 'application/json'); 2653 | http.send(message); 2654 | } 2655 | } 2656 | 2657 | Socket.prototype.close = function() { 2658 | if (!this.disconnected && this._wsOpen()) { 2659 | this._socket.close(); 2660 | this.disconnected = true; 2661 | } 2662 | } 2663 | 2664 | })(this); 2665 | --------------------------------------------------------------------------------