From 648bb2f07106c63b0f69c89053936ca7a6d54f8f Mon Sep 17 00:00:00 2001 From: Micael Gallego Date: Sun, 16 Oct 2016 23:16:58 +0200 Subject: [PATCH] Use NPM dependencies for openvidu-browser This avoid to include several scripts in every webpage using OpenVidu. It also make easier to update dependency versions. --- .../src/main/resources/.gitignore | 2 + .../src/main/resources/package.json | 20 + .../src/main/resources/static/js/OpenVidu.js | 16791 +++++++++++++++- .../src/main/resources/static/ts/Main.ts | 4 - .../main/resources/static/ts_js/OpenVidu.js | 1086 - .../resources/static/ts_js/OpenVidu.js.map | 1 - .../src/main/resources/ts/..jsOpenVidu.js | 1 + .../main/resources/ts/..staticjsOpenVidu.js | 16047 +++++++++++++++ .../src/main/resources/ts/Main.ts | 12 + .../resources/{static => }/ts/OpenVidu.ts | 4 +- .../resources/{static => }/ts/Participant.ts | 0 .../main/resources/{static => }/ts/Session.ts | 3 +- .../main/resources/{static => }/ts/Stream.ts | 32 +- .../src/main/resources/ts/definitions.d.ts | 3 + .../resources/{static => }/ts/tsconfig.json | 2 +- .../src/main/resources/static/index.html | 16 +- .../main/resources/static/js/EventEmitter.js | 472 - .../src/main/resources/static/js/adapter.js | 536 - .../resources/static/js/jquery-2.1.1.min.js | 4 - .../resources/static/js/kurento-jsonrpc.js | 1403 -- .../src/main/resources/static/js/testapp.js | 5 +- 21 files changed, 31987 insertions(+), 4457 deletions(-) create mode 100644 openvidu-browser/src/main/resources/.gitignore create mode 100644 openvidu-browser/src/main/resources/package.json delete mode 100644 openvidu-browser/src/main/resources/static/ts/Main.ts delete mode 100644 openvidu-browser/src/main/resources/static/ts_js/OpenVidu.js delete mode 100644 openvidu-browser/src/main/resources/static/ts_js/OpenVidu.js.map create mode 100644 openvidu-browser/src/main/resources/ts/..jsOpenVidu.js create mode 100644 openvidu-browser/src/main/resources/ts/..staticjsOpenVidu.js create mode 100644 openvidu-browser/src/main/resources/ts/Main.ts rename openvidu-browser/src/main/resources/{static => }/ts/OpenVidu.ts (99%) rename openvidu-browser/src/main/resources/{static => }/ts/Participant.ts (100%) rename openvidu-browser/src/main/resources/{static => }/ts/Session.ts (99%) rename openvidu-browser/src/main/resources/{static => }/ts/Stream.ts (94%) create mode 100644 openvidu-browser/src/main/resources/ts/definitions.d.ts rename openvidu-browser/src/main/resources/{static => }/ts/tsconfig.json (96%) delete mode 100644 openvidu-testapp/src/main/resources/static/js/EventEmitter.js delete mode 100644 openvidu-testapp/src/main/resources/static/js/adapter.js delete mode 100644 openvidu-testapp/src/main/resources/static/js/jquery-2.1.1.min.js delete mode 100644 openvidu-testapp/src/main/resources/static/js/kurento-jsonrpc.js diff --git a/openvidu-browser/src/main/resources/.gitignore b/openvidu-browser/src/main/resources/.gitignore new file mode 100644 index 00000000..5e760f68 --- /dev/null +++ b/openvidu-browser/src/main/resources/.gitignore @@ -0,0 +1,2 @@ +/node_modules/ +/yarn.lock diff --git a/openvidu-browser/src/main/resources/package.json b/openvidu-browser/src/main/resources/package.json new file mode 100644 index 00000000..4a3c481a --- /dev/null +++ b/openvidu-browser/src/main/resources/package.json @@ -0,0 +1,20 @@ +{ + "name": "openvidu", + "version": "0.0.1", + "description": "OpenVidu Browser", + "main": "OpenVidu.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "kurento-jsonrpc": "5.1.3", + "wolfy87-eventemitter": "4.2.9", + "@types/wolfy87-eventemitter": "4.2.31", + "webrtc-adapter":"2.0.4", + "kurento-utils":"6.6.0", + "uuid": "~2.0.1", + "sdp-translator": "^0.1.15" + } +} diff --git a/openvidu-browser/src/main/resources/static/js/OpenVidu.js b/openvidu-browser/src/main/resources/static/js/OpenVidu.js index 10906034..3eb77473 100644 --- a/openvidu-browser/src/main/resources/static/js/OpenVidu.js +++ b/openvidu-browser/src/main/resources/static/js/OpenVidu.js @@ -1,3 +1,14533 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + +function useColors() { + // is webkit? http://stackoverflow.com/a/16459606/376773 + return ('WebkitAppearance' in document.documentElement.style) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (window.console && (console.firebug || (console.exception && console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31); +} + +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + +exports.formatters.j = function(v) { + return JSON.stringify(v); +}; + + +/** + * Colorize log arguments if enabled. + * + * @api public + */ + +function formatArgs() { + var args = arguments; + var useColors = this.useColors; + + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); + + if (!useColors) return args; + + var c = 'color: ' + this.color; + args = [args[0], c, 'color: inherit'].concat(Array.prototype.slice.call(args, 1)); + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); + return args; +} + +/** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ + +function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} + return r; +} + +/** + * Enable namespaces listed in `localStorage.debug` initially. + */ + +exports.enable(load()); + +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + +function localstorage(){ + try { + return window.localStorage; + } catch (e) {} +} + +},{"./debug":2}],2:[function(require,module,exports){ + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = debug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = require('ms'); + +/** + * The currently active debug mode names, and names to skip. + */ + +exports.names = []; +exports.skips = []; + +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lowercased letter, i.e. "n". + */ + +exports.formatters = {}; + +/** + * Previously assigned color. + */ + +var prevColor = 0; + +/** + * Previous log timestamp. + */ + +var prevTime; + +/** + * Select a color. + * + * @return {Number} + * @api private + */ + +function selectColor() { + return exports.colors[prevColor++ % exports.colors.length]; +} + +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + +function debug(namespace) { + + // define the `disabled` version + function disabled() { + } + disabled.enabled = false; + + // define the `enabled` version + function enabled() { + + var self = enabled; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // add the `color` if not set + if (null == self.useColors) self.useColors = exports.useColors(); + if (null == self.color && self.useColors) self.color = selectColor(); + + var args = Array.prototype.slice.call(arguments); + + args[0] = exports.coerce(args[0]); + + if ('string' !== typeof args[0]) { + // anything else let's inspect with %o + args = ['%o'].concat(args); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + if ('function' === typeof exports.formatArgs) { + args = exports.formatArgs.apply(self, args); + } + var logFn = enabled.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } + enabled.enabled = true; + + var fn = exports.enabled(namespace) ? enabled : disabled; + + fn.namespace = namespace; + + return fn; +} + +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + +function enable(namespaces) { + exports.save(namespaces); + + var split = (namespaces || '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } +} + +/** + * Disable debug output. + * + * @api public + */ + +function disable() { + exports.enable(''); +} + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} + +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + +},{"ms":21}],3:[function(require,module,exports){ +/* jshint node: true */ +'use strict'; + +var normalice = require('normalice'); + +/** + # freeice + + The `freeice` module is a simple way of getting random STUN or TURN server + for your WebRTC application. The list of servers (just STUN at this stage) + were sourced from this [gist](https://gist.github.com/zziuni/3741933). + + ## Example Use + + The following demonstrates how you can use `freeice` with + [rtc-quickconnect](https://github.com/rtc-io/rtc-quickconnect): + + <<< examples/quickconnect.js + + As the `freeice` module generates ice servers in a list compliant with the + WebRTC spec you will be able to use it with raw `RTCPeerConnection` + constructors and other WebRTC libraries. + + ## Hey, don't use my STUN/TURN server! + + If for some reason your free STUN or TURN server ends up in the + list of servers ([stun](https://github.com/DamonOehlman/freeice/blob/master/stun.json) or + [turn](https://github.com/DamonOehlman/freeice/blob/master/turn.json)) + that is used in this module, you can feel + free to open an issue on this repository and those servers will be removed + within 24 hours (or sooner). This is the quickest and probably the most + polite way to have something removed (and provides us some visibility + if someone opens a pull request requesting that a server is added). + + ## Please add my server! + + If you have a server that you wish to add to the list, that's awesome! I'm + sure I speak on behalf of a whole pile of WebRTC developers who say thanks. + To get it into the list, feel free to either open a pull request or if you + find that process a bit daunting then just create an issue requesting + the addition of the server (make sure you provide all the details, and if + you have a Terms of Service then including that in the PR/issue would be + awesome). + + ## I know of a free server, can I add it? + + Sure, if you do your homework and make sure it is ok to use (I'm currently + in the process of reviewing the terms of those STUN servers included from + the original list). If it's ok to go, then please see the previous entry + for how to add it. + + ## Current List of Servers + + * current as at the time of last `README.md` file generation + + ### STUN + + <<< stun.json + + ### TURN + + <<< turn.json + +**/ + +var freeice = module.exports = function(opts) { + // if a list of servers has been provided, then use it instead of defaults + var servers = { + stun: (opts || {}).stun || require('./stun.json'), + turn: (opts || {}).turn || require('./turn.json') + }; + + var stunCount = (opts || {}).stunCount || 2; + var turnCount = (opts || {}).turnCount || 0; + var selected; + + function getServers(type, count) { + var out = []; + var input = [].concat(servers[type]); + var idx; + + while (input.length && out.length < count) { + idx = (Math.random() * input.length) | 0; + out = out.concat(input.splice(idx, 1)); + } + + return out.map(function(url) { + //If it's a not a string, don't try to "normalice" it otherwise using type:url will screw it up + if ((typeof url !== 'string') && (! (url instanceof String))) { + return url; + } else { + return normalice(type + ':' + url); + } + }); + } + + // add stun servers + selected = [].concat(getServers('stun', stunCount)); + + if (turnCount) { + selected = selected.concat(getServers('turn', turnCount)); + } + + return selected; +}; + +},{"./stun.json":4,"./turn.json":5,"normalice":22}],4:[function(require,module,exports){ +module.exports=[ + "stun.l.google.com:19302", + "stun1.l.google.com:19302", + "stun2.l.google.com:19302", + "stun3.l.google.com:19302", + "stun4.l.google.com:19302", + "stun.ekiga.net", + "stun.ideasip.com", + "stun.schlund.de", + "stun.stunprotocol.org:3478", + "stun.voiparound.com", + "stun.voipbuster.com", + "stun.voipstunt.com", + "stun.voxgratia.org", + "stun.services.mozilla.com" +] + +},{}],5:[function(require,module,exports){ +module.exports=[] + +},{}],6:[function(require,module,exports){ +var WildEmitter = require('wildemitter'); + +function getMaxVolume (analyser, fftBins) { + var maxVolume = -Infinity; + analyser.getFloatFrequencyData(fftBins); + + for(var i=4, ii=fftBins.length; i < ii; i++) { + if (fftBins[i] > maxVolume && fftBins[i] < 0) { + maxVolume = fftBins[i]; + } + }; + + return maxVolume; +} + + +var audioContextType = window.AudioContext || window.webkitAudioContext; +// use a single audio context due to hardware limits +var audioContext = null; +module.exports = function(stream, options) { + var harker = new WildEmitter(); + + + // make it not break in non-supported browsers + if (!audioContextType) return harker; + + //Config + var options = options || {}, + smoothing = (options.smoothing || 0.1), + interval = (options.interval || 50), + threshold = options.threshold, + play = options.play, + history = options.history || 10, + running = true; + + //Setup Audio Context + if (!audioContext) { + audioContext = new audioContextType(); + } + var sourceNode, fftBins, analyser; + + analyser = audioContext.createAnalyser(); + analyser.fftSize = 512; + analyser.smoothingTimeConstant = smoothing; + fftBins = new Float32Array(analyser.fftSize); + + if (stream.jquery) stream = stream[0]; + if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) { + //Audio Tag + sourceNode = audioContext.createMediaElementSource(stream); + if (typeof play === 'undefined') play = true; + threshold = threshold || -50; + } else { + //WebRTC Stream + sourceNode = audioContext.createMediaStreamSource(stream); + threshold = threshold || -50; + } + + sourceNode.connect(analyser); + if (play) analyser.connect(audioContext.destination); + + harker.speaking = false; + + harker.setThreshold = function(t) { + threshold = t; + }; + + harker.setInterval = function(i) { + interval = i; + }; + + harker.stop = function() { + running = false; + harker.emit('volume_change', -100, threshold); + if (harker.speaking) { + harker.speaking = false; + harker.emit('stopped_speaking'); + } + }; + harker.speakingHistory = []; + for (var i = 0; i < history; i++) { + harker.speakingHistory.push(0); + } + + // Poll the analyser node to determine if speaking + // and emit events if changed + var looper = function() { + setTimeout(function() { + + //check if stop has been called + if(!running) { + return; + } + + var currentVolume = getMaxVolume(analyser, fftBins); + + harker.emit('volume_change', currentVolume, threshold); + + var history = 0; + if (currentVolume > threshold && !harker.speaking) { + // trigger quickly, short history + for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) { + history += harker.speakingHistory[i]; + } + if (history >= 2) { + harker.speaking = true; + harker.emit('speaking'); + } + } else if (currentVolume < threshold && harker.speaking) { + for (var i = 0; i < harker.speakingHistory.length; i++) { + history += harker.speakingHistory[i]; + } + if (history == 0) { + harker.speaking = false; + harker.emit('stopped_speaking'); + } + } + harker.speakingHistory.shift(); + harker.speakingHistory.push(0 + (currentVolume > threshold)); + + looper(); + }, interval); + }; + looper(); + + + return harker; +} + +},{"wildemitter":101}],7:[function(require,module,exports){ +if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + }; +} else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } +} + +},{}],8:[function(require,module,exports){ +(function (global){ +/*! JSON v3.3.2 | http://bestiejs.github.io/json3 | Copyright 2012-2014, Kit Cambridge | http://kit.mit-license.org */ +;(function () { + // Detect the `define` function exposed by asynchronous module loaders. The + // strict `define` check is necessary for compatibility with `r.js`. + var isLoader = typeof define === "function" && define.amd; + + // A set of types used to distinguish objects from primitives. + var objectTypes = { + "function": true, + "object": true + }; + + // Detect the `exports` object exposed by CommonJS implementations. + var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports; + + // Use the `global` object exposed by Node (including Browserify via + // `insert-module-globals`), Narwhal, and Ringo as the default context, + // and the `window` object in browsers. Rhino exports a `global` function + // instead. + var root = objectTypes[typeof window] && window || this, + freeGlobal = freeExports && objectTypes[typeof module] && module && !module.nodeType && typeof global == "object" && global; + + if (freeGlobal && (freeGlobal["global"] === freeGlobal || freeGlobal["window"] === freeGlobal || freeGlobal["self"] === freeGlobal)) { + root = freeGlobal; + } + + // Public: Initializes JSON 3 using the given `context` object, attaching the + // `stringify` and `parse` functions to the specified `exports` object. + function runInContext(context, exports) { + context || (context = root["Object"]()); + exports || (exports = root["Object"]()); + + // Native constructor aliases. + var Number = context["Number"] || root["Number"], + String = context["String"] || root["String"], + Object = context["Object"] || root["Object"], + Date = context["Date"] || root["Date"], + SyntaxError = context["SyntaxError"] || root["SyntaxError"], + TypeError = context["TypeError"] || root["TypeError"], + Math = context["Math"] || root["Math"], + nativeJSON = context["JSON"] || root["JSON"]; + + // Delegate to the native `stringify` and `parse` implementations. + if (typeof nativeJSON == "object" && nativeJSON) { + exports.stringify = nativeJSON.stringify; + exports.parse = nativeJSON.parse; + } + + // Convenience aliases. + var objectProto = Object.prototype, + getClass = objectProto.toString, + isProperty, forEach, undef; + + // Test the `Date#getUTC*` methods. Based on work by @Yaffle. + var isExtended = new Date(-3509827334573292); + try { + // The `getUTCFullYear`, `Month`, and `Date` methods return nonsensical + // results for certain dates in Opera >= 10.53. + isExtended = isExtended.getUTCFullYear() == -109252 && isExtended.getUTCMonth() === 0 && isExtended.getUTCDate() === 1 && + // Safari < 2.0.2 stores the internal millisecond time value correctly, + // but clips the values returned by the date methods to the range of + // signed 32-bit integers ([-2 ** 31, 2 ** 31 - 1]). + isExtended.getUTCHours() == 10 && isExtended.getUTCMinutes() == 37 && isExtended.getUTCSeconds() == 6 && isExtended.getUTCMilliseconds() == 708; + } catch (exception) {} + + // Internal: Determines whether the native `JSON.stringify` and `parse` + // implementations are spec-compliant. Based on work by Ken Snyder. + function has(name) { + if (has[name] !== undef) { + // Return cached feature test result. + return has[name]; + } + var isSupported; + if (name == "bug-string-char-index") { + // IE <= 7 doesn't support accessing string characters using square + // bracket notation. IE 8 only supports this for primitives. + isSupported = "a"[0] != "a"; + } else if (name == "json") { + // Indicates whether both `JSON.stringify` and `JSON.parse` are + // supported. + isSupported = has("json-stringify") && has("json-parse"); + } else { + var value, serialized = '{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}'; + // Test `JSON.stringify`. + if (name == "json-stringify") { + var stringify = exports.stringify, stringifySupported = typeof stringify == "function" && isExtended; + if (stringifySupported) { + // A test function object with a custom `toJSON` method. + (value = function () { + return 1; + }).toJSON = value; + try { + stringifySupported = + // Firefox 3.1b1 and b2 serialize string, number, and boolean + // primitives as object literals. + stringify(0) === "0" && + // FF 3.1b1, b2, and JSON 2 serialize wrapped primitives as object + // literals. + stringify(new Number()) === "0" && + stringify(new String()) == '""' && + // FF 3.1b1, 2 throw an error if the value is `null`, `undefined`, or + // does not define a canonical JSON representation (this applies to + // objects with `toJSON` properties as well, *unless* they are nested + // within an object or array). + stringify(getClass) === undef && + // IE 8 serializes `undefined` as `"undefined"`. Safari <= 5.1.7 and + // FF 3.1b3 pass this test. + stringify(undef) === undef && + // Safari <= 5.1.7 and FF 3.1b3 throw `Error`s and `TypeError`s, + // respectively, if the value is omitted entirely. + stringify() === undef && + // FF 3.1b1, 2 throw an error if the given value is not a number, + // string, array, object, Boolean, or `null` literal. This applies to + // objects with custom `toJSON` methods as well, unless they are nested + // inside object or array literals. YUI 3.0.0b1 ignores custom `toJSON` + // methods entirely. + stringify(value) === "1" && + stringify([value]) == "[1]" && + // Prototype <= 1.6.1 serializes `[undefined]` as `"[]"` instead of + // `"[null]"`. + stringify([undef]) == "[null]" && + // YUI 3.0.0b1 fails to serialize `null` literals. + stringify(null) == "null" && + // FF 3.1b1, 2 halts serialization if an array contains a function: + // `[1, true, getClass, 1]` serializes as "[1,true,],". FF 3.1b3 + // elides non-JSON values from objects and arrays, unless they + // define custom `toJSON` methods. + stringify([undef, getClass, null]) == "[null,null,null]" && + // Simple serialization test. FF 3.1b1 uses Unicode escape sequences + // where character escape codes are expected (e.g., `\b` => `\u0008`). + stringify({ "a": [value, true, false, null, "\x00\b\n\f\r\t"] }) == serialized && + // FF 3.1b1 and b2 ignore the `filter` and `width` arguments. + stringify(null, value) === "1" && + stringify([1, 2], null, 1) == "[\n 1,\n 2\n]" && + // JSON 2, Prototype <= 1.7, and older WebKit builds incorrectly + // serialize extended years. + stringify(new Date(-8.64e15)) == '"-271821-04-20T00:00:00.000Z"' && + // The milliseconds are optional in ES 5, but required in 5.1. + stringify(new Date(8.64e15)) == '"+275760-09-13T00:00:00.000Z"' && + // Firefox <= 11.0 incorrectly serializes years prior to 0 as negative + // four-digit years instead of six-digit years. Credits: @Yaffle. + stringify(new Date(-621987552e5)) == '"-000001-01-01T00:00:00.000Z"' && + // Safari <= 5.1.5 and Opera >= 10.53 incorrectly serialize millisecond + // values less than 1000. Credits: @Yaffle. + stringify(new Date(-1)) == '"1969-12-31T23:59:59.999Z"'; + } catch (exception) { + stringifySupported = false; + } + } + isSupported = stringifySupported; + } + // Test `JSON.parse`. + if (name == "json-parse") { + var parse = exports.parse; + if (typeof parse == "function") { + try { + // FF 3.1b1, b2 will throw an exception if a bare literal is provided. + // Conforming implementations should also coerce the initial argument to + // a string prior to parsing. + if (parse("0") === 0 && !parse(false)) { + // Simple parsing test. + value = parse(serialized); + var parseSupported = value["a"].length == 5 && value["a"][0] === 1; + if (parseSupported) { + try { + // Safari <= 5.1.2 and FF 3.1b1 allow unescaped tabs in strings. + parseSupported = !parse('"\t"'); + } catch (exception) {} + if (parseSupported) { + try { + // FF 4.0 and 4.0.1 allow leading `+` signs and leading + // decimal points. FF 4.0, 4.0.1, and IE 9-10 also allow + // certain octal literals. + parseSupported = parse("01") !== 1; + } catch (exception) {} + } + if (parseSupported) { + try { + // FF 4.0, 4.0.1, and Rhino 1.7R3-R4 allow trailing decimal + // points. These environments, along with FF 3.1b1 and 2, + // also allow trailing commas in JSON objects and arrays. + parseSupported = parse("1.") !== 1; + } catch (exception) {} + } + } + } + } catch (exception) { + parseSupported = false; + } + } + isSupported = parseSupported; + } + } + return has[name] = !!isSupported; + } + + if (!has("json")) { + // Common `[[Class]]` name aliases. + var functionClass = "[object Function]", + dateClass = "[object Date]", + numberClass = "[object Number]", + stringClass = "[object String]", + arrayClass = "[object Array]", + booleanClass = "[object Boolean]"; + + // Detect incomplete support for accessing string characters by index. + var charIndexBuggy = has("bug-string-char-index"); + + // Define additional utility methods if the `Date` methods are buggy. + if (!isExtended) { + var floor = Math.floor; + // A mapping between the months of the year and the number of days between + // January 1st and the first of the respective month. + var Months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; + // Internal: Calculates the number of days between the Unix epoch and the + // first day of the given month. + var getDay = function (year, month) { + return Months[month] + 365 * (year - 1970) + floor((year - 1969 + (month = +(month > 1))) / 4) - floor((year - 1901 + month) / 100) + floor((year - 1601 + month) / 400); + }; + } + + // Internal: Determines if a property is a direct property of the given + // object. Delegates to the native `Object#hasOwnProperty` method. + if (!(isProperty = objectProto.hasOwnProperty)) { + isProperty = function (property) { + var members = {}, constructor; + if ((members.__proto__ = null, members.__proto__ = { + // The *proto* property cannot be set multiple times in recent + // versions of Firefox and SeaMonkey. + "toString": 1 + }, members).toString != getClass) { + // Safari <= 2.0.3 doesn't implement `Object#hasOwnProperty`, but + // supports the mutable *proto* property. + isProperty = function (property) { + // Capture and break the object's prototype chain (see section 8.6.2 + // of the ES 5.1 spec). The parenthesized expression prevents an + // unsafe transformation by the Closure Compiler. + var original = this.__proto__, result = property in (this.__proto__ = null, this); + // Restore the original prototype chain. + this.__proto__ = original; + return result; + }; + } else { + // Capture a reference to the top-level `Object` constructor. + constructor = members.constructor; + // Use the `constructor` property to simulate `Object#hasOwnProperty` in + // other environments. + isProperty = function (property) { + var parent = (this.constructor || constructor).prototype; + return property in this && !(property in parent && this[property] === parent[property]); + }; + } + members = null; + return isProperty.call(this, property); + }; + } + + // Internal: Normalizes the `for...in` iteration algorithm across + // environments. Each enumerated key is yielded to a `callback` function. + forEach = function (object, callback) { + var size = 0, Properties, members, property; + + // Tests for bugs in the current environment's `for...in` algorithm. The + // `valueOf` property inherits the non-enumerable flag from + // `Object.prototype` in older versions of IE, Netscape, and Mozilla. + (Properties = function () { + this.valueOf = 0; + }).prototype.valueOf = 0; + + // Iterate over a new instance of the `Properties` class. + members = new Properties(); + for (property in members) { + // Ignore all properties inherited from `Object.prototype`. + if (isProperty.call(members, property)) { + size++; + } + } + Properties = members = null; + + // Normalize the iteration algorithm. + if (!size) { + // A list of non-enumerable properties inherited from `Object.prototype`. + members = ["valueOf", "toString", "toLocaleString", "propertyIsEnumerable", "isPrototypeOf", "hasOwnProperty", "constructor"]; + // IE <= 8, Mozilla 1.0, and Netscape 6.2 ignore shadowed non-enumerable + // properties. + forEach = function (object, callback) { + var isFunction = getClass.call(object) == functionClass, property, length; + var hasProperty = !isFunction && typeof object.constructor != "function" && objectTypes[typeof object.hasOwnProperty] && object.hasOwnProperty || isProperty; + for (property in object) { + // Gecko <= 1.0 enumerates the `prototype` property of functions under + // certain conditions; IE does not. + if (!(isFunction && property == "prototype") && hasProperty.call(object, property)) { + callback(property); + } + } + // Manually invoke the callback for each non-enumerable property. + for (length = members.length; property = members[--length]; hasProperty.call(object, property) && callback(property)); + }; + } else if (size == 2) { + // Safari <= 2.0.4 enumerates shadowed properties twice. + forEach = function (object, callback) { + // Create a set of iterated properties. + var members = {}, isFunction = getClass.call(object) == functionClass, property; + for (property in object) { + // Store each property name to prevent double enumeration. The + // `prototype` property of functions is not enumerated due to cross- + // environment inconsistencies. + if (!(isFunction && property == "prototype") && !isProperty.call(members, property) && (members[property] = 1) && isProperty.call(object, property)) { + callback(property); + } + } + }; + } else { + // No bugs detected; use the standard `for...in` algorithm. + forEach = function (object, callback) { + var isFunction = getClass.call(object) == functionClass, property, isConstructor; + for (property in object) { + if (!(isFunction && property == "prototype") && isProperty.call(object, property) && !(isConstructor = property === "constructor")) { + callback(property); + } + } + // Manually invoke the callback for the `constructor` property due to + // cross-environment inconsistencies. + if (isConstructor || isProperty.call(object, (property = "constructor"))) { + callback(property); + } + }; + } + return forEach(object, callback); + }; + + // Public: Serializes a JavaScript `value` as a JSON string. The optional + // `filter` argument may specify either a function that alters how object and + // array members are serialized, or an array of strings and numbers that + // indicates which properties should be serialized. The optional `width` + // argument may be either a string or number that specifies the indentation + // level of the output. + if (!has("json-stringify")) { + // Internal: A map of control characters and their escaped equivalents. + var Escapes = { + 92: "\\\\", + 34: '\\"', + 8: "\\b", + 12: "\\f", + 10: "\\n", + 13: "\\r", + 9: "\\t" + }; + + // Internal: Converts `value` into a zero-padded string such that its + // length is at least equal to `width`. The `width` must be <= 6. + var leadingZeroes = "000000"; + var toPaddedString = function (width, value) { + // The `|| 0` expression is necessary to work around a bug in + // Opera <= 7.54u2 where `0 == -0`, but `String(-0) !== "0"`. + return (leadingZeroes + (value || 0)).slice(-width); + }; + + // Internal: Double-quotes a string `value`, replacing all ASCII control + // characters (characters with code unit values between 0 and 31) with + // their escaped equivalents. This is an implementation of the + // `Quote(value)` operation defined in ES 5.1 section 15.12.3. + var unicodePrefix = "\\u00"; + var quote = function (value) { + var result = '"', index = 0, length = value.length, useCharIndex = !charIndexBuggy || length > 10; + var symbols = useCharIndex && (charIndexBuggy ? value.split("") : value); + for (; index < length; index++) { + var charCode = value.charCodeAt(index); + // If the character is a control character, append its Unicode or + // shorthand escape sequence; otherwise, append the character as-is. + switch (charCode) { + case 8: case 9: case 10: case 12: case 13: case 34: case 92: + result += Escapes[charCode]; + break; + default: + if (charCode < 32) { + result += unicodePrefix + toPaddedString(2, charCode.toString(16)); + break; + } + result += useCharIndex ? symbols[index] : value.charAt(index); + } + } + return result + '"'; + }; + + // Internal: Recursively serializes an object. Implements the + // `Str(key, holder)`, `JO(value)`, and `JA(value)` operations. + var serialize = function (property, object, callback, properties, whitespace, indentation, stack) { + var value, className, year, month, date, time, hours, minutes, seconds, milliseconds, results, element, index, length, prefix, result; + try { + // Necessary for host object support. + value = object[property]; + } catch (exception) {} + if (typeof value == "object" && value) { + className = getClass.call(value); + if (className == dateClass && !isProperty.call(value, "toJSON")) { + if (value > -1 / 0 && value < 1 / 0) { + // Dates are serialized according to the `Date#toJSON` method + // specified in ES 5.1 section 15.9.5.44. See section 15.9.1.15 + // for the ISO 8601 date time string format. + if (getDay) { + // Manually compute the year, month, date, hours, minutes, + // seconds, and milliseconds if the `getUTC*` methods are + // buggy. Adapted from @Yaffle's `date-shim` project. + date = floor(value / 864e5); + for (year = floor(date / 365.2425) + 1970 - 1; getDay(year + 1, 0) <= date; year++); + for (month = floor((date - getDay(year, 0)) / 30.42); getDay(year, month + 1) <= date; month++); + date = 1 + date - getDay(year, month); + // The `time` value specifies the time within the day (see ES + // 5.1 section 15.9.1.2). The formula `(A % B + B) % B` is used + // to compute `A modulo B`, as the `%` operator does not + // correspond to the `modulo` operation for negative numbers. + time = (value % 864e5 + 864e5) % 864e5; + // The hours, minutes, seconds, and milliseconds are obtained by + // decomposing the time within the day. See section 15.9.1.10. + hours = floor(time / 36e5) % 24; + minutes = floor(time / 6e4) % 60; + seconds = floor(time / 1e3) % 60; + milliseconds = time % 1e3; + } else { + year = value.getUTCFullYear(); + month = value.getUTCMonth(); + date = value.getUTCDate(); + hours = value.getUTCHours(); + minutes = value.getUTCMinutes(); + seconds = value.getUTCSeconds(); + milliseconds = value.getUTCMilliseconds(); + } + // Serialize extended years correctly. + value = (year <= 0 || year >= 1e4 ? (year < 0 ? "-" : "+") + toPaddedString(6, year < 0 ? -year : year) : toPaddedString(4, year)) + + "-" + toPaddedString(2, month + 1) + "-" + toPaddedString(2, date) + + // Months, dates, hours, minutes, and seconds should have two + // digits; milliseconds should have three. + "T" + toPaddedString(2, hours) + ":" + toPaddedString(2, minutes) + ":" + toPaddedString(2, seconds) + + // Milliseconds are optional in ES 5.0, but required in 5.1. + "." + toPaddedString(3, milliseconds) + "Z"; + } else { + value = null; + } + } else if (typeof value.toJSON == "function" && ((className != numberClass && className != stringClass && className != arrayClass) || isProperty.call(value, "toJSON"))) { + // Prototype <= 1.6.1 adds non-standard `toJSON` methods to the + // `Number`, `String`, `Date`, and `Array` prototypes. JSON 3 + // ignores all `toJSON` methods on these objects unless they are + // defined directly on an instance. + value = value.toJSON(property); + } + } + if (callback) { + // If a replacement function was provided, call it to obtain the value + // for serialization. + value = callback.call(object, property, value); + } + if (value === null) { + return "null"; + } + className = getClass.call(value); + if (className == booleanClass) { + // Booleans are represented literally. + return "" + value; + } else if (className == numberClass) { + // JSON numbers must be finite. `Infinity` and `NaN` are serialized as + // `"null"`. + return value > -1 / 0 && value < 1 / 0 ? "" + value : "null"; + } else if (className == stringClass) { + // Strings are double-quoted and escaped. + return quote("" + value); + } + // Recursively serialize objects and arrays. + if (typeof value == "object") { + // Check for cyclic structures. This is a linear search; performance + // is inversely proportional to the number of unique nested objects. + for (length = stack.length; length--;) { + if (stack[length] === value) { + // Cyclic structures cannot be serialized by `JSON.stringify`. + throw TypeError(); + } + } + // Add the object to the stack of traversed objects. + stack.push(value); + results = []; + // Save the current indentation level and indent one additional level. + prefix = indentation; + indentation += whitespace; + if (className == arrayClass) { + // Recursively serialize array elements. + for (index = 0, length = value.length; index < length; index++) { + element = serialize(index, value, callback, properties, whitespace, indentation, stack); + results.push(element === undef ? "null" : element); + } + result = results.length ? (whitespace ? "[\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "]" : ("[" + results.join(",") + "]")) : "[]"; + } else { + // Recursively serialize object members. Members are selected from + // either a user-specified list of property names, or the object + // itself. + forEach(properties || value, function (property) { + var element = serialize(property, value, callback, properties, whitespace, indentation, stack); + if (element !== undef) { + // According to ES 5.1 section 15.12.3: "If `gap` {whitespace} + // is not the empty string, let `member` {quote(property) + ":"} + // be the concatenation of `member` and the `space` character." + // The "`space` character" refers to the literal space + // character, not the `space` {width} argument provided to + // `JSON.stringify`. + results.push(quote(property) + ":" + (whitespace ? " " : "") + element); + } + }); + result = results.length ? (whitespace ? "{\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "}" : ("{" + results.join(",") + "}")) : "{}"; + } + // Remove the object from the traversed object stack. + stack.pop(); + return result; + } + }; + + // Public: `JSON.stringify`. See ES 5.1 section 15.12.3. + exports.stringify = function (source, filter, width) { + var whitespace, callback, properties, className; + if (objectTypes[typeof filter] && filter) { + if ((className = getClass.call(filter)) == functionClass) { + callback = filter; + } else if (className == arrayClass) { + // Convert the property names array into a makeshift set. + properties = {}; + for (var index = 0, length = filter.length, value; index < length; value = filter[index++], ((className = getClass.call(value)), className == stringClass || className == numberClass) && (properties[value] = 1)); + } + } + if (width) { + if ((className = getClass.call(width)) == numberClass) { + // Convert the `width` to an integer and create a string containing + // `width` number of space characters. + if ((width -= width % 1) > 0) { + for (whitespace = "", width > 10 && (width = 10); whitespace.length < width; whitespace += " "); + } + } else if (className == stringClass) { + whitespace = width.length <= 10 ? width : width.slice(0, 10); + } + } + // Opera <= 7.54u2 discards the values associated with empty string keys + // (`""`) only if they are used directly within an object member list + // (e.g., `!("" in { "": 1})`). + return serialize("", (value = {}, value[""] = source, value), callback, properties, whitespace, "", []); + }; + } + + // Public: Parses a JSON source string. + if (!has("json-parse")) { + var fromCharCode = String.fromCharCode; + + // Internal: A map of escaped control characters and their unescaped + // equivalents. + var Unescapes = { + 92: "\\", + 34: '"', + 47: "/", + 98: "\b", + 116: "\t", + 110: "\n", + 102: "\f", + 114: "\r" + }; + + // Internal: Stores the parser state. + var Index, Source; + + // Internal: Resets the parser state and throws a `SyntaxError`. + var abort = function () { + Index = Source = null; + throw SyntaxError(); + }; + + // Internal: Returns the next token, or `"$"` if the parser has reached + // the end of the source string. A token may be a string, number, `null` + // literal, or Boolean literal. + var lex = function () { + var source = Source, length = source.length, value, begin, position, isSigned, charCode; + while (Index < length) { + charCode = source.charCodeAt(Index); + switch (charCode) { + case 9: case 10: case 13: case 32: + // Skip whitespace tokens, including tabs, carriage returns, line + // feeds, and space characters. + Index++; + break; + case 123: case 125: case 91: case 93: case 58: case 44: + // Parse a punctuator token (`{`, `}`, `[`, `]`, `:`, or `,`) at + // the current position. + value = charIndexBuggy ? source.charAt(Index) : source[Index]; + Index++; + return value; + case 34: + // `"` delimits a JSON string; advance to the next character and + // begin parsing the string. String tokens are prefixed with the + // sentinel `@` character to distinguish them from punctuators and + // end-of-string tokens. + for (value = "@", Index++; Index < length;) { + charCode = source.charCodeAt(Index); + if (charCode < 32) { + // Unescaped ASCII control characters (those with a code unit + // less than the space character) are not permitted. + abort(); + } else if (charCode == 92) { + // A reverse solidus (`\`) marks the beginning of an escaped + // control character (including `"`, `\`, and `/`) or Unicode + // escape sequence. + charCode = source.charCodeAt(++Index); + switch (charCode) { + case 92: case 34: case 47: case 98: case 116: case 110: case 102: case 114: + // Revive escaped control characters. + value += Unescapes[charCode]; + Index++; + break; + case 117: + // `\u` marks the beginning of a Unicode escape sequence. + // Advance to the first character and validate the + // four-digit code point. + begin = ++Index; + for (position = Index + 4; Index < position; Index++) { + charCode = source.charCodeAt(Index); + // A valid sequence comprises four hexdigits (case- + // insensitive) that form a single hexadecimal value. + if (!(charCode >= 48 && charCode <= 57 || charCode >= 97 && charCode <= 102 || charCode >= 65 && charCode <= 70)) { + // Invalid Unicode escape sequence. + abort(); + } + } + // Revive the escaped character. + value += fromCharCode("0x" + source.slice(begin, Index)); + break; + default: + // Invalid escape sequence. + abort(); + } + } else { + if (charCode == 34) { + // An unescaped double-quote character marks the end of the + // string. + break; + } + charCode = source.charCodeAt(Index); + begin = Index; + // Optimize for the common case where a string is valid. + while (charCode >= 32 && charCode != 92 && charCode != 34) { + charCode = source.charCodeAt(++Index); + } + // Append the string as-is. + value += source.slice(begin, Index); + } + } + if (source.charCodeAt(Index) == 34) { + // Advance to the next character and return the revived string. + Index++; + return value; + } + // Unterminated string. + abort(); + default: + // Parse numbers and literals. + begin = Index; + // Advance past the negative sign, if one is specified. + if (charCode == 45) { + isSigned = true; + charCode = source.charCodeAt(++Index); + } + // Parse an integer or floating-point value. + if (charCode >= 48 && charCode <= 57) { + // Leading zeroes are interpreted as octal literals. + if (charCode == 48 && ((charCode = source.charCodeAt(Index + 1)), charCode >= 48 && charCode <= 57)) { + // Illegal octal literal. + abort(); + } + isSigned = false; + // Parse the integer component. + for (; Index < length && ((charCode = source.charCodeAt(Index)), charCode >= 48 && charCode <= 57); Index++); + // Floats cannot contain a leading decimal point; however, this + // case is already accounted for by the parser. + if (source.charCodeAt(Index) == 46) { + position = ++Index; + // Parse the decimal component. + for (; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++); + if (position == Index) { + // Illegal trailing decimal. + abort(); + } + Index = position; + } + // Parse exponents. The `e` denoting the exponent is + // case-insensitive. + charCode = source.charCodeAt(Index); + if (charCode == 101 || charCode == 69) { + charCode = source.charCodeAt(++Index); + // Skip past the sign following the exponent, if one is + // specified. + if (charCode == 43 || charCode == 45) { + Index++; + } + // Parse the exponential component. + for (position = Index; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++); + if (position == Index) { + // Illegal empty exponent. + abort(); + } + Index = position; + } + // Coerce the parsed value to a JavaScript number. + return +source.slice(begin, Index); + } + // A negative sign may only precede numbers. + if (isSigned) { + abort(); + } + // `true`, `false`, and `null` literals. + if (source.slice(Index, Index + 4) == "true") { + Index += 4; + return true; + } else if (source.slice(Index, Index + 5) == "false") { + Index += 5; + return false; + } else if (source.slice(Index, Index + 4) == "null") { + Index += 4; + return null; + } + // Unrecognized token. + abort(); + } + } + // Return the sentinel `$` character if the parser has reached the end + // of the source string. + return "$"; + }; + + // Internal: Parses a JSON `value` token. + var get = function (value) { + var results, hasMembers; + if (value == "$") { + // Unexpected end of input. + abort(); + } + if (typeof value == "string") { + if ((charIndexBuggy ? value.charAt(0) : value[0]) == "@") { + // Remove the sentinel `@` character. + return value.slice(1); + } + // Parse object and array literals. + if (value == "[") { + // Parses a JSON array, returning a new JavaScript array. + results = []; + for (;; hasMembers || (hasMembers = true)) { + value = lex(); + // A closing square bracket marks the end of the array literal. + if (value == "]") { + break; + } + // If the array literal contains elements, the current token + // should be a comma separating the previous element from the + // next. + if (hasMembers) { + if (value == ",") { + value = lex(); + if (value == "]") { + // Unexpected trailing `,` in array literal. + abort(); + } + } else { + // A `,` must separate each array element. + abort(); + } + } + // Elisions and leading commas are not permitted. + if (value == ",") { + abort(); + } + results.push(get(value)); + } + return results; + } else if (value == "{") { + // Parses a JSON object, returning a new JavaScript object. + results = {}; + for (;; hasMembers || (hasMembers = true)) { + value = lex(); + // A closing curly brace marks the end of the object literal. + if (value == "}") { + break; + } + // If the object literal contains members, the current token + // should be a comma separator. + if (hasMembers) { + if (value == ",") { + value = lex(); + if (value == "}") { + // Unexpected trailing `,` in object literal. + abort(); + } + } else { + // A `,` must separate each object member. + abort(); + } + } + // Leading commas are not permitted, object property names must be + // double-quoted strings, and a `:` must separate each property + // name and value. + if (value == "," || typeof value != "string" || (charIndexBuggy ? value.charAt(0) : value[0]) != "@" || lex() != ":") { + abort(); + } + results[value.slice(1)] = get(lex()); + } + return results; + } + // Unexpected token encountered. + abort(); + } + return value; + }; + + // Internal: Updates a traversed object member. + var update = function (source, property, callback) { + var element = walk(source, property, callback); + if (element === undef) { + delete source[property]; + } else { + source[property] = element; + } + }; + + // Internal: Recursively traverses a parsed JSON object, invoking the + // `callback` function for each value. This is an implementation of the + // `Walk(holder, name)` operation defined in ES 5.1 section 15.12.2. + var walk = function (source, property, callback) { + var value = source[property], length; + if (typeof value == "object" && value) { + // `forEach` can't be used to traverse an array in Opera <= 8.54 + // because its `Object#hasOwnProperty` implementation returns `false` + // for array indices (e.g., `![1, 2, 3].hasOwnProperty("0")`). + if (getClass.call(value) == arrayClass) { + for (length = value.length; length--;) { + update(value, length, callback); + } + } else { + forEach(value, function (property) { + update(value, property, callback); + }); + } + } + return callback.call(source, property, value); + }; + + // Public: `JSON.parse`. See ES 5.1 section 15.12.2. + exports.parse = function (source, callback) { + var result, value; + Index = 0; + Source = "" + source; + result = get(lex()); + // If a JSON string contains multiple tokens, it is invalid. + if (lex() != "$") { + abort(); + } + // Reset the parser state. + Index = Source = null; + return callback && getClass.call(callback) == functionClass ? walk((value = {}, value[""] = result, value), "", callback) : result; + }; + } + } + + exports["runInContext"] = runInContext; + return exports; + } + + if (freeExports && !isLoader) { + // Export for CommonJS environments. + runInContext(root, freeExports); + } else { + // Export for web browsers and JavaScript engines. + var nativeJSON = root.JSON, + previousJSON = root["JSON3"], + isRestored = false; + + var JSON3 = runInContext(root, (root["JSON3"] = { + // Public: Restores the original value of the global `JSON` object and + // returns a reference to the `JSON3` object. + "noConflict": function () { + if (!isRestored) { + isRestored = true; + root.JSON = nativeJSON; + root["JSON3"] = previousJSON; + nativeJSON = previousJSON = null; + } + return JSON3; + } + })); + + root.JSON = { + "parse": JSON3.parse, + "stringify": JSON3.stringify + }; + } + + // Export for asynchronous module loaders. + if (isLoader) { + define(function () { + return JSON3; + }); + } +}).call(this); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{}],9:[function(require,module,exports){ +function Mapper() +{ + var sources = {}; + + + this.forEach = function(callback) + { + for(var key in sources) + { + var source = sources[key]; + + for(var key2 in source) + callback(source[key2]); + }; + }; + + this.get = function(id, source) + { + var ids = sources[source]; + if(ids == undefined) + return undefined; + + return ids[id]; + }; + + this.remove = function(id, source) + { + var ids = sources[source]; + if(ids == undefined) + return; + + delete ids[id]; + + // Check it's empty + for(var i in ids){return false} + + delete sources[source]; + }; + + this.set = function(value, id, source) + { + if(value == undefined) + return this.remove(id, source); + + var ids = sources[source]; + if(ids == undefined) + sources[source] = ids = {}; + + ids[id] = value; + }; +}; + + +Mapper.prototype.pop = function(id, source) +{ + var value = this.get(id, source); + if(value == undefined) + return undefined; + + this.remove(id, source); + + return value; +}; + + +module.exports = Mapper; + +},{}],10:[function(require,module,exports){ +/* + * (C) Copyright 2014 Kurento (http://kurento.org/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var JsonRpcClient = require('./jsonrpcclient'); + + +exports.JsonRpcClient = JsonRpcClient; +},{"./jsonrpcclient":11}],11:[function(require,module,exports){ +/* + * (C) Copyright 2014 Kurento (http://kurento.org/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var RpcBuilder = require('../..'); +var WebSocketWithReconnection = require('./transports/webSocketWithReconnection'); + +Date.now = Date.now || function() { + return +new Date; +}; + +var PING_INTERVAL = 5000; + +var RECONNECTING = 'RECONNECTING'; +var CONNECTED = 'CONNECTED'; +var DISCONNECTED = 'DISCONNECTED'; + +var RECONNECTING = "RECONNECTING"; +var CONNECTED = "CONNECTED"; +var DISCONNECTED = "DISCONNECTED"; + + +/** + * + * heartbeat: interval in ms for each heartbeat message, + * sendCloseMessage : true / false, before closing the connection, it sends a closeSession message + *
+ * ws : {
+ * 	uri : URI to conntect to,
+ *  useSockJS : true (use SockJS) / false (use WebSocket) by default,
+ * 	onconnected : callback method to invoke when connection is successful,
+ * 	ondisconnect : callback method to invoke when the connection is lost,
+ * 	onreconnecting : callback method to invoke when the client is reconnecting,
+ * 	onreconnected : callback method to invoke when the client succesfully reconnects,
+ * },
+ * rpc : {
+ * 	requestTimeout : timeout for a request,
+ * 	sessionStatusChanged: callback method for changes in session status,
+ * 	mediaRenegotiation: mediaRenegotiation
+ * }
+ * 
+ */ +function JsonRpcClient(configuration) { + + var self = this; + + var wsConfig = configuration.ws; + + var notReconnectIfNumLessThan = -1; + + var pingNextNum = 0; + var enabledPings = true; + var pingPongStarted = false; + var pingInterval; + + var status = DISCONNECTED; + + var onreconnecting = wsConfig.onreconnecting; + var onreconnected = wsConfig.onreconnected; + var onconnected = wsConfig.onconnected; + + configuration.rpc.pull = function(params, request) { + request.reply(null, "push"); + } + + wsConfig.onreconnecting = function() { + console.log("--------- ONRECONNECTING -----------"); + if (status === RECONNECTING) { + console.error("Websocket already in RECONNECTING state when receiving a new ONRECONNECTING message. Ignoring it"); + return; + } + + status = RECONNECTING; + if (onreconnecting) { + onreconnecting(); + } + } + + wsConfig.onreconnected = function() { + console.log("--------- ONRECONNECTED -----------"); + if (status === CONNECTED) { + console.error("Websocket already in CONNECTED state when receiving a new ONRECONNECTED message. Ignoring it"); + return; + } + status = CONNECTED; + + enabledPings = true; + updateNotReconnectIfLessThan(); + usePing(); + + if (onreconnected) { + onreconnected(); + } + } + + wsConfig.onconnected = function() { + console.log("--------- ONCONNECTED -----------"); + if (status === CONNECTED) { + console.error("Websocket already in CONNECTED state when receiving a new ONCONNECTED message. Ignoring it"); + return; + } + status = CONNECTED; + + enabledPings = true; + usePing(); + + if (onconnected) { + onconnected(); + } + } + + var ws = new WebSocketWithReconnection(wsConfig); + + console.log('Connecting websocket to URI: ' + wsConfig.uri); + + var rpcBuilderOptions = { + request_timeout: configuration.rpc.requestTimeout + }; + + var rpc = new RpcBuilder(RpcBuilder.packers.JsonRPC, rpcBuilderOptions, ws, + function(request) { + + console.log('Received request: ' + JSON.stringify(request)); + + try { + var func = configuration.rpc[request.method]; + + if (func === undefined) { + console.error("Method " + request.method + " not registered in client"); + } else { + func(request.params, request); + } + } catch (err) { + console.error('Exception processing request: ' + JSON.stringify(request)); + console.error(err); + } + }); + + this.send = function(method, params, callback) { + if (method !== 'ping') { + console.log('Request: method:' + method + " params:" + JSON.stringify(params)); + } + + var requestTime = Date.now(); + + rpc.encode(method, params, function(error, result) { + if (error) { + try { + console.error("ERROR:" + error.message + " in Request: method:" + method + " params:" + JSON.stringify(params)); + if (error.data) { + console.error("ERROR DATA:" + JSON.stringify(error.data)); + } + } catch (e) {} + error.requestTime = requestTime; + } + if (callback) { + if (result != undefined && result.value !== 'pong') { + console.log('Response: ' + JSON.stringify(result)); + } + callback(error, result); + } + }); + } + + function updateNotReconnectIfLessThan() { + notReconnectIfNumLessThan = pingNextNum; + console.log("notReconnectIfNumLessThan = " + notReconnectIfNumLessThan); + } + + function sendPing() { + if (enabledPings) { + var params = null; + + if (pingNextNum == 0 || pingNextNum == notReconnectIfNumLessThan) { + params = { + interval: PING_INTERVAL + }; + } + + pingNextNum++; + + self.send('ping', params, (function(pingNum) { + return function(error, result) { + if (error) { + if (pingNum > notReconnectIfNumLessThan) { + enabledPings = false; + updateNotReconnectIfLessThan(); + console.log("DSS did not respond to ping message " + pingNum + ". Reconnecting... "); + ws.reconnectWs(); + } + } + } + })(pingNextNum)); + } else { + console.log("Trying to send ping, but ping is not enabled"); + } + } + + /* + * If configuration.hearbeat has any value, the ping-pong will work with the interval + * of configuration.hearbeat + */ + function usePing() { + if (!pingPongStarted) { + console.log("Starting ping (if configured)") + pingPongStarted = true; + + if (configuration.heartbeat != undefined) { + pingInterval = setInterval(sendPing, configuration.heartbeat); + sendPing(); + } + } + } + + this.close = function() { + console.log("Closing jsonRpcClient explicitely by client"); + + if (pingInterval != undefined) { + clearInterval(pingInterval); + } + pingPongStarted = false; + enabledPings = false; + + if (configuration.sendCloseMessage) { + this.send('closeSession', null, function(error, result) { + if (error) { + console.error("Error sending close message: " + JSON.stringify(error)); + } + + ws.close(); + }); + } else { + ws.close(); + } + } + + // This method is only for testing + this.forceClose = function(millis) { + ws.forceClose(millis); + } + + this.reconnect = function() { + ws.reconnectWs(); + } +} + + +module.exports = JsonRpcClient; + +},{"../..":14,"./transports/webSocketWithReconnection":13}],12:[function(require,module,exports){ +/* + * (C) Copyright 2014 Kurento (http://kurento.org/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var WebSocketWithReconnection = require('./webSocketWithReconnection'); + + +exports.WebSocketWithReconnection = WebSocketWithReconnection; +},{"./webSocketWithReconnection":13}],13:[function(require,module,exports){ +/* + * (C) Copyright 2013-2015 Kurento (http://kurento.org/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use strict"; + +var WebSocket = require('ws'); +var SockJS = require('sockjs-client'); + +var MAX_RETRIES = 2000; // Forever... +var RETRY_TIME_MS = 3000; // FIXME: Implement exponential wait times... +var PING_INTERVAL = 5000; +var PING_MSG = JSON.stringify({ + 'method': 'ping' +}); + +var CONNECTING = 0; +var OPEN = 1; +var CLOSING = 2; +var CLOSED = 3; + +/* +config = { + uri : wsUri, + useSockJS : true (use SockJS) / false (use WebSocket) by default, + onconnected : callback method to invoke when connection is successful, + ondisconnect : callback method to invoke when the connection is lost, + onreconnecting : callback method to invoke when the client is reconnecting, + onreconnected : callback method to invoke when the client succesfully reconnects, + }; +*/ +function WebSocketWithReconnection(config) { + + var closing = false; + var registerMessageHandler; + var wsUri = config.uri; + var useSockJS = config.useSockJS; + var reconnecting = false; + + var forcingDisconnection = false; + + var ws; + + if (useSockJS) { + ws = new SockJS(wsUri); + } else { + ws = new WebSocket(wsUri); + } + + ws.onopen = function() { + logConnected(ws, wsUri); + config.onconnected(); + }; + + ws.onerror = function(evt) { + config.onconnected(evt.data); + }; + + function logConnected(ws, wsUri) { + try { + console.log("WebSocket connected to " + wsUri); + } catch (e) { + console.error(e); + } + } + + var reconnectionOnClose = function() { + if (ws.readyState === CLOSED) { + if (closing) { + console.log("Connection Closed by user"); + } else { + console.log("Connection closed unexpectecly. Reconnecting..."); + reconnectInNewUri(MAX_RETRIES, 1); + } + } else { + console.log("Close callback from previous websocket. Ignoring it"); + } + }; + + ws.onclose = reconnectionOnClose; + + function reconnectInNewUri(maxRetries, numRetries) { + console.log("reconnectInNewUri"); + + if (numRetries === 1) { + if (reconnecting) { + console + .warn("Trying to reconnect when reconnecting... Ignoring this reconnection.") + return; + } else { + reconnecting = true; + } + + if (config.onreconnecting) { + config.onreconnecting(); + } + } + + if (forcingDisconnection) { + reconnect(maxRetries, numRetries, wsUri); + + } else { + if (config.newWsUriOnReconnection) { + config.newWsUriOnReconnection(function(error, newWsUri) { + + if (error) { + console.log(error); + setTimeout(function() { + reconnectInNewUri(maxRetries, numRetries + 1); + }, RETRY_TIME_MS); + } else { + reconnect(maxRetries, numRetries, newWsUri); + } + }) + } else { + reconnect(maxRetries, numRetries, wsUri); + } + } + } + + // TODO Test retries. How to force not connection? + function reconnect(maxRetries, numRetries, reconnectWsUri) { + + console.log("Trying to reconnect " + numRetries + " times"); + + var newWs; + if (useSockJS) { + newWs = new SockJS(wsUri); + } else { + newWs = new WebSocket(wsUri); + } + + newWs.onopen = function() { + console.log("Reconnected in " + numRetries + " retries..."); + logConnected(newWs, reconnectWsUri); + reconnecting = false; + registerMessageHandler(); + if (config.onreconnected()) { + config.onreconnected(); + } + + newWs.onclose = reconnectionOnClose; + }; + + var onErrorOrClose = function(error) { + console.log("Reconnection error: ", error); + + if (numRetries === maxRetries) { + if (config.ondisconnect) { + config.ondisconnect(); + } + } else { + setTimeout(function() { + reconnectInNewUri(maxRetries, numRetries + 1); + }, RETRY_TIME_MS); + } + }; + + newWs.onerror = onErrorOrClose; + + ws = newWs; + } + + this.close = function() { + closing = true; + ws.close(); + }; + + + // This method is only for testing + this.forceClose = function(millis) { + console.log("Testing: Force WebSocket close"); + + if (millis) { + console.log("Testing: Change wsUri for " + millis + " millis to simulate net failure"); + var goodWsUri = wsUri; + wsUri = "wss://21.234.12.34.4:443/"; + + forcingDisconnection = true; + + setTimeout(function() { + console.log("Testing: Recover good wsUri " + goodWsUri); + wsUri = goodWsUri; + + forcingDisconnection = false; + + }, millis); + } + + ws.close(); + }; + + this.reconnectWs = function() { + console.log("reconnectWs"); + reconnectInNewUri(MAX_RETRIES, 1, wsUri); + }; + + this.send = function(message) { + ws.send(message); + }; + + this.addEventListener = function(type, callback) { + registerMessageHandler = function() { + ws.addEventListener(type, callback); + }; + + registerMessageHandler(); + }; +} + +module.exports = WebSocketWithReconnection; +},{"sockjs-client":34,"ws":103}],14:[function(require,module,exports){ +/* + * (C) Copyright 2014 Kurento (http://kurento.org/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + + +var defineProperty_IE8 = false +if(Object.defineProperty) +{ + try + { + Object.defineProperty({}, "x", {}); + } + catch(e) + { + defineProperty_IE8 = true + } +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind +if (!Function.prototype.bind) { + Function.prototype.bind = function(oThis) { + if (typeof this !== 'function') { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function() {}, + fBound = function() { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +} + + +var EventEmitter = require('events').EventEmitter; + +var inherits = require('inherits'); + +var packers = require('./packers'); +var Mapper = require('./Mapper'); + + +var BASE_TIMEOUT = 5000; + + +function unifyResponseMethods(responseMethods) +{ + if(!responseMethods) return {}; + + for(var key in responseMethods) + { + var value = responseMethods[key]; + + if(typeof value == 'string') + responseMethods[key] = + { + response: value + } + }; + + return responseMethods; +}; + +function unifyTransport(transport) +{ + if(!transport) return; + + // Transport as a function + if(transport instanceof Function) + return {send: transport}; + + // WebSocket & DataChannel + if(transport.send instanceof Function) + return transport; + + // Message API (Inter-window & WebWorker) + if(transport.postMessage instanceof Function) + { + transport.send = transport.postMessage; + return transport; + } + + // Stream API + if(transport.write instanceof Function) + { + transport.send = transport.write; + return transport; + } + + // Transports that only can receive messages, but not send + if(transport.onmessage !== undefined) return; + if(transport.pause instanceof Function) return; + + throw new SyntaxError("Transport is not a function nor a valid object"); +}; + + +/** + * Representation of a RPC notification + * + * @class + * + * @constructor + * + * @param {String} method -method of the notification + * @param params - parameters of the notification + */ +function RpcNotification(method, params) +{ + if(defineProperty_IE8) + { + this.method = method + this.params = params + } + else + { + Object.defineProperty(this, 'method', {value: method, enumerable: true}); + Object.defineProperty(this, 'params', {value: params, enumerable: true}); + } +}; + + +/** + * @class + * + * @constructor + * + * @param {object} packer + * + * @param {object} [options] + * + * @param {object} [transport] + * + * @param {Function} [onRequest] + */ +function RpcBuilder(packer, options, transport, onRequest) +{ + var self = this; + + if(!packer) + throw new SyntaxError('Packer is not defined'); + + if(!packer.pack || !packer.unpack) + throw new SyntaxError('Packer is invalid'); + + var responseMethods = unifyResponseMethods(packer.responseMethods); + + + if(options instanceof Function) + { + if(transport != undefined) + throw new SyntaxError("There can't be parameters after onRequest"); + + onRequest = options; + transport = undefined; + options = undefined; + }; + + if(options && options.send instanceof Function) + { + if(transport && !(transport instanceof Function)) + throw new SyntaxError("Only a function can be after transport"); + + onRequest = transport; + transport = options; + options = undefined; + }; + + if(transport instanceof Function) + { + if(onRequest != undefined) + throw new SyntaxError("There can't be parameters after onRequest"); + + onRequest = transport; + transport = undefined; + }; + + if(transport && transport.send instanceof Function) + if(onRequest && !(onRequest instanceof Function)) + throw new SyntaxError("Only a function can be after transport"); + + options = options || {}; + + + EventEmitter.call(this); + + if(onRequest) + this.on('request', onRequest); + + + if(defineProperty_IE8) + this.peerID = options.peerID + else + Object.defineProperty(this, 'peerID', {value: options.peerID}); + + var max_retries = options.max_retries || 0; + + + function transportMessage(event) + { + self.decode(event.data || event); + }; + + this.getTransport = function() + { + return transport; + } + this.setTransport = function(value) + { + // Remove listener from old transport + if(transport) + { + // W3C transports + if(transport.removeEventListener) + transport.removeEventListener('message', transportMessage); + + // Node.js Streams API + else if(transport.removeListener) + transport.removeListener('data', transportMessage); + }; + + // Set listener on new transport + if(value) + { + // W3C transports + if(value.addEventListener) + value.addEventListener('message', transportMessage); + + // Node.js Streams API + else if(value.addListener) + value.addListener('data', transportMessage); + }; + + transport = unifyTransport(value); + } + + if(!defineProperty_IE8) + Object.defineProperty(this, 'transport', + { + get: this.getTransport.bind(this), + set: this.setTransport.bind(this) + }) + + this.setTransport(transport); + + + var request_timeout = options.request_timeout || BASE_TIMEOUT; + var response_timeout = options.response_timeout || BASE_TIMEOUT; + var duplicates_timeout = options.duplicates_timeout || BASE_TIMEOUT; + + + var requestID = 0; + + var requests = new Mapper(); + var responses = new Mapper(); + var processedResponses = new Mapper(); + + var message2Key = {}; + + + /** + * Store the response to prevent to process duplicate request later + */ + function storeResponse(message, id, dest) + { + var response = + { + message: message, + /** Timeout to auto-clean old responses */ + timeout: setTimeout(function() + { + responses.remove(id, dest); + }, + response_timeout) + }; + + responses.set(response, id, dest); + }; + + /** + * Store the response to ignore duplicated messages later + */ + function storeProcessedResponse(ack, from) + { + var timeout = setTimeout(function() + { + processedResponses.remove(ack, from); + }, + duplicates_timeout); + + processedResponses.set(timeout, ack, from); + }; + + + /** + * Representation of a RPC request + * + * @class + * @extends RpcNotification + * + * @constructor + * + * @param {String} method -method of the notification + * @param params - parameters of the notification + * @param {Integer} id - identifier of the request + * @param [from] - source of the notification + */ + function RpcRequest(method, params, id, from, transport) + { + RpcNotification.call(this, method, params); + + this.getTransport = function() + { + return transport; + } + this.setTransport = function(value) + { + transport = unifyTransport(value); + } + + if(!defineProperty_IE8) + Object.defineProperty(this, 'transport', + { + get: this.getTransport.bind(this), + set: this.setTransport.bind(this) + }) + + var response = responses.get(id, from); + + /** + * @constant {Boolean} duplicated + */ + if(!(transport || self.getTransport())) + { + if(defineProperty_IE8) + this.duplicated = Boolean(response) + else + Object.defineProperty(this, 'duplicated', + { + value: Boolean(response) + }); + } + + var responseMethod = responseMethods[method]; + + this.pack = packer.pack.bind(packer, this, id) + + /** + * Generate a response to this request + * + * @param {Error} [error] + * @param {*} [result] + * + * @returns {string} + */ + this.reply = function(error, result, transport) + { + // Fix optional parameters + if(error instanceof Function || error && error.send instanceof Function) + { + if(result != undefined) + throw new SyntaxError("There can't be parameters after callback"); + + transport = error; + result = null; + error = undefined; + } + + else if(result instanceof Function + || result && result.send instanceof Function) + { + if(transport != undefined) + throw new SyntaxError("There can't be parameters after callback"); + + transport = result; + result = null; + }; + + transport = unifyTransport(transport); + + // Duplicated request, remove old response timeout + if(response) + clearTimeout(response.timeout); + + if(from != undefined) + { + if(error) + error.dest = from; + + if(result) + result.dest = from; + }; + + var message; + + // New request or overriden one, create new response with provided data + if(error || result != undefined) + { + if(self.peerID != undefined) + { + if(error) + error.from = self.peerID; + else + result.from = self.peerID; + } + + // Protocol indicates that responses has own request methods + if(responseMethod) + { + if(responseMethod.error == undefined && error) + message = + { + error: error + }; + + else + { + var method = error + ? responseMethod.error + : responseMethod.response; + + message = + { + method: method, + params: error || result + }; + } + } + else + message = + { + error: error, + result: result + }; + + message = packer.pack(message, id); + } + + // Duplicate & not-overriden request, re-send old response + else if(response) + message = response.message; + + // New empty reply, response null value + else + message = packer.pack({result: null}, id); + + // Store the response to prevent to process a duplicated request later + storeResponse(message, id, from); + + // Return the stored response so it can be directly send back + transport = transport || this.getTransport() || self.getTransport(); + + if(transport) + return transport.send(message); + + return message; + } + }; + inherits(RpcRequest, RpcNotification); + + + function cancel(message) + { + var key = message2Key[message]; + if(!key) return; + + delete message2Key[message]; + + var request = requests.pop(key.id, key.dest); + if(!request) return; + + clearTimeout(request.timeout); + + // Start duplicated responses timeout + storeProcessedResponse(key.id, key.dest); + }; + + /** + * Allow to cancel a request and don't wait for a response + * + * If `message` is not given, cancel all the request + */ + this.cancel = function(message) + { + if(message) return cancel(message); + + for(var message in message2Key) + cancel(message); + }; + + + this.close = function() + { + // Prevent to receive new messages + var transport = this.getTransport(); + if(transport && transport.close) + transport.close(); + + // Request & processed responses + this.cancel(); + + processedResponses.forEach(clearTimeout); + + // Responses + responses.forEach(function(response) + { + clearTimeout(response.timeout); + }); + }; + + + /** + * Generates and encode a JsonRPC 2.0 message + * + * @param {String} method -method of the notification + * @param params - parameters of the notification + * @param [dest] - destination of the notification + * @param {object} [transport] - transport where to send the message + * @param [callback] - function called when a response to this request is + * received. If not defined, a notification will be send instead + * + * @returns {string} A raw JsonRPC 2.0 request or notification string + */ + this.encode = function(method, params, dest, transport, callback) + { + // Fix optional parameters + if(params instanceof Function) + { + if(dest != undefined) + throw new SyntaxError("There can't be parameters after callback"); + + callback = params; + transport = undefined; + dest = undefined; + params = undefined; + } + + else if(dest instanceof Function) + { + if(transport != undefined) + throw new SyntaxError("There can't be parameters after callback"); + + callback = dest; + transport = undefined; + dest = undefined; + } + + else if(transport instanceof Function) + { + if(callback != undefined) + throw new SyntaxError("There can't be parameters after callback"); + + callback = transport; + transport = undefined; + }; + + if(self.peerID != undefined) + { + params = params || {}; + + params.from = self.peerID; + }; + + if(dest != undefined) + { + params = params || {}; + + params.dest = dest; + }; + + // Encode message + var message = + { + method: method, + params: params + }; + + if(callback) + { + var id = requestID++; + var retried = 0; + + message = packer.pack(message, id); + + function dispatchCallback(error, result) + { + self.cancel(message); + + callback(error, result); + }; + + var request = + { + message: message, + callback: dispatchCallback, + responseMethods: responseMethods[method] || {} + }; + + var encode_transport = unifyTransport(transport); + + function sendRequest(transport) + { + request.timeout = setTimeout(timeout, + request_timeout*Math.pow(2, retried++)); + message2Key[message] = {id: id, dest: dest}; + requests.set(request, id, dest); + + transport = transport || encode_transport || self.getTransport(); + if(transport) + return transport.send(message); + + return message; + }; + + function retry(transport) + { + transport = unifyTransport(transport); + + console.warn(retried+' retry for request message:',message); + + var timeout = processedResponses.pop(id, dest); + clearTimeout(timeout); + + return sendRequest(transport); + }; + + function timeout() + { + if(retried < max_retries) + return retry(transport); + + var error = new Error('Request has timed out'); + error.request = message; + + error.retry = retry; + + dispatchCallback(error) + }; + + return sendRequest(transport); + }; + + // Return the packed message + message = packer.pack(message); + + transport = transport || this.getTransport(); + if(transport) + return transport.send(message); + + return message; + }; + + /** + * Decode and process a JsonRPC 2.0 message + * + * @param {string} message - string with the content of the message + * + * @returns {RpcNotification|RpcRequest|undefined} - the representation of the + * notification or the request. If a response was processed, it will return + * `undefined` to notify that it was processed + * + * @throws {TypeError} - Message is not defined + */ + this.decode = function(message, transport) + { + if(!message) + throw new TypeError("Message is not defined"); + + try + { + message = packer.unpack(message); + } + catch(e) + { + // Ignore invalid messages + return console.log(e, message); + }; + + var id = message.id; + var ack = message.ack; + var method = message.method; + var params = message.params || {}; + + var from = params.from; + var dest = params.dest; + + // Ignore messages send by us + if(self.peerID != undefined && from == self.peerID) return; + + // Notification + if(id == undefined && ack == undefined) + { + var notification = new RpcNotification(method, params); + + if(self.emit('request', notification)) return; + return notification; + }; + + + function processRequest() + { + // If we have a transport and it's a duplicated request, reply inmediatly + transport = unifyTransport(transport) || self.getTransport(); + if(transport) + { + var response = responses.get(id, from); + if(response) + return transport.send(response.message); + }; + + var idAck = (id != undefined) ? id : ack; + var request = new RpcRequest(method, params, idAck, from, transport); + + if(self.emit('request', request)) return; + return request; + }; + + function processResponse(request, error, result) + { + request.callback(error, result); + }; + + function duplicatedResponse(timeout) + { + console.warn("Response already processed", message); + + // Update duplicated responses timeout + clearTimeout(timeout); + storeProcessedResponse(ack, from); + }; + + + // Request, or response with own method + if(method) + { + // Check if it's a response with own method + if(dest == undefined || dest == self.peerID) + { + var request = requests.get(ack, from); + if(request) + { + var responseMethods = request.responseMethods; + + if(method == responseMethods.error) + return processResponse(request, params); + + if(method == responseMethods.response) + return processResponse(request, null, params); + + return processRequest(); + } + + var processed = processedResponses.get(ack, from); + if(processed) + return duplicatedResponse(processed); + } + + // Request + return processRequest(); + }; + + var error = message.error; + var result = message.result; + + // Ignore responses not send to us + if(error && error.dest && error.dest != self.peerID) return; + if(result && result.dest && result.dest != self.peerID) return; + + // Response + var request = requests.get(ack, from); + if(!request) + { + var processed = processedResponses.get(ack, from); + if(processed) + return duplicatedResponse(processed); + + return console.warn("No callback was defined for this message", message); + }; + + // Process response + processResponse(request, error, result); + }; +}; +inherits(RpcBuilder, EventEmitter); + + +RpcBuilder.RpcNotification = RpcNotification; + + +module.exports = RpcBuilder; + +var clients = require('./clients'); +var transports = require('./clients/transports'); + +RpcBuilder.clients = clients; +RpcBuilder.clients.transports = transports; +RpcBuilder.packers = packers; + +},{"./Mapper":9,"./clients":10,"./clients/transports":12,"./packers":17,"events":109,"inherits":7}],15:[function(require,module,exports){ +/** + * JsonRPC 2.0 packer + */ + +/** + * Pack a JsonRPC 2.0 message + * + * @param {Object} message - object to be packaged. It requires to have all the + * fields needed by the JsonRPC 2.0 message that it's going to be generated + * + * @return {String} - the stringified JsonRPC 2.0 message + */ +function pack(message, id) +{ + var result = + { + jsonrpc: "2.0" + }; + + // Request + if(message.method) + { + result.method = message.method; + + if(message.params) + result.params = message.params; + + // Request is a notification + if(id != undefined) + result.id = id; + } + + // Response + else if(id != undefined) + { + if(message.error) + { + if(message.result !== undefined) + throw new TypeError("Both result and error are defined"); + + result.error = message.error; + } + else if(message.result !== undefined) + result.result = message.result; + else + throw new TypeError("No result or error is defined"); + + result.id = id; + }; + + return JSON.stringify(result); +}; + +/** + * Unpack a JsonRPC 2.0 message + * + * @param {String} message - string with the content of the JsonRPC 2.0 message + * + * @throws {TypeError} - Invalid JsonRPC version + * + * @return {Object} - object filled with the JsonRPC 2.0 message content + */ +function unpack(message) +{ + var result = message; + + if(typeof message === 'string' || message instanceof String) + result = JSON.parse(message); + + // Check if it's a valid message + + var version = result.jsonrpc; + if(version !== '2.0') + throw new TypeError("Invalid JsonRPC version '" + version + "': " + message); + + // Response + if(result.method == undefined) + { + if(result.id == undefined) + throw new TypeError("Invalid message: "+message); + + var result_defined = result.result !== undefined; + var error_defined = result.error !== undefined; + + // Check only result or error is defined, not both or none + if(result_defined && error_defined) + throw new TypeError("Both result and error are defined: "+message); + + if(!result_defined && !error_defined) + throw new TypeError("No result or error is defined: "+message); + + result.ack = result.id; + delete result.id; + } + + // Return unpacked message + return result; +}; + + +exports.pack = pack; +exports.unpack = unpack; + +},{}],16:[function(require,module,exports){ +function pack(message) +{ + throw new TypeError("Not yet implemented"); +}; + +function unpack(message) +{ + throw new TypeError("Not yet implemented"); +}; + + +exports.pack = pack; +exports.unpack = unpack; + +},{}],17:[function(require,module,exports){ +var JsonRPC = require('./JsonRPC'); +var XmlRPC = require('./XmlRPC'); + + +exports.JsonRPC = JsonRPC; +exports.XmlRPC = XmlRPC; + +},{"./JsonRPC":15,"./XmlRPC":16}],18:[function(require,module,exports){ +/* + * (C) Copyright 2014-2015 Kurento (http://kurento.org/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var freeice = require('freeice') +var inherits = require('inherits') +var UAParser = require('ua-parser-js') +var uuid = require('uuid') +var hark = require('hark') + +var EventEmitter = require('events').EventEmitter +var recursive = require('merge').recursive.bind(undefined, true) +var sdpTranslator = require('sdp-translator') + +try { + require('kurento-browser-extensions') +} catch (error) { + if (typeof getScreenConstraints === 'undefined') { + console.warn('screen sharing is not available') + + getScreenConstraints = function getScreenConstraints(sendSource, callback) { + callback(new Error("This library is not enabled for screen sharing")) + } + } +} + +var MEDIA_CONSTRAINTS = { + audio: true, + video: { + width: 640, + framerate: 15 + } +} + +// Somehow, the UAParser constructor gets an empty window object. +// We need to pass the user agent string in order to get information +var ua = (window && window.navigator) ? window.navigator.userAgent : '' +var parser = new UAParser(ua) +var browser = parser.getBrowser() + +var usePlanB = false +if (browser.name === 'Chrome' || browser.name === 'Chromium') { + console.log(browser.name + ": using SDP PlanB") + usePlanB = true +} + +function noop(error) { + if (error) console.error(error) +} + +function trackStop(track) { + track.stop && track.stop() +} + +function streamStop(stream) { + stream.getTracks().forEach(trackStop) +} + +/** + * Returns a string representation of a SessionDescription object. + */ +var dumpSDP = function (description) { + if (typeof description === 'undefined' || description === null) { + return '' + } + + return 'type: ' + description.type + '\r\n' + description.sdp +} + +function bufferizeCandidates(pc, onerror) { + var candidatesQueue = [] + + pc.addEventListener('signalingstatechange', function () { + if (this.signalingState === 'stable') { + while (candidatesQueue.length) { + var entry = candidatesQueue.shift() + + this.addIceCandidate(entry.candidate, entry.callback, entry.callback) + } + } + }) + + return function (candidate, callback) { + callback = callback || onerror + + switch (pc.signalingState) { + case 'closed': + callback(new Error('PeerConnection object is closed')) + break + case 'stable': + if (pc.remoteDescription) { + pc.addIceCandidate(candidate, callback, callback) + break + } + default: + candidatesQueue.push({ + candidate: candidate, + callback: callback + }) + } + } +} + +/* Simulcast utilities */ + +function removeFIDFromOffer(sdp) { + var n = sdp.indexOf("a=ssrc-group:FID"); + + if (n > 0) { + return sdp.slice(0, n); + } else { + return sdp; + } +} + +function getSimulcastInfo(videoStream) { + var videoTracks = videoStream.getVideoTracks(); + if (!videoTracks.length) { + console.warn('No video tracks available in the video stream') + return '' + } + var lines = [ + 'a=x-google-flag:conference', + 'a=ssrc-group:SIM 1 2 3', + 'a=ssrc:1 cname:localVideo', + 'a=ssrc:1 msid:' + videoStream.id + ' ' + videoTracks[0].id, + 'a=ssrc:1 mslabel:' + videoStream.id, + 'a=ssrc:1 label:' + videoTracks[0].id, + 'a=ssrc:2 cname:localVideo', + 'a=ssrc:2 msid:' + videoStream.id + ' ' + videoTracks[0].id, + 'a=ssrc:2 mslabel:' + videoStream.id, + 'a=ssrc:2 label:' + videoTracks[0].id, + 'a=ssrc:3 cname:localVideo', + 'a=ssrc:3 msid:' + videoStream.id + ' ' + videoTracks[0].id, + 'a=ssrc:3 mslabel:' + videoStream.id, + 'a=ssrc:3 label:' + videoTracks[0].id + ]; + + lines.push(''); + + return lines.join('\n'); +} + +/** + * Wrapper object of an RTCPeerConnection. This object is aimed to simplify the + * development of WebRTC-based applications. + * + * @constructor module:kurentoUtils.WebRtcPeer + * + * @param {String} mode Mode in which the PeerConnection will be configured. + * Valid values are: 'recv', 'send', and 'sendRecv' + * @param localVideo Video tag for the local stream + * @param remoteVideo Video tag for the remote stream + * @param {MediaStream} videoStream Stream to be used as primary source + * (typically video and audio, or only video if combined with audioStream) for + * localVideo and to be added as stream to the RTCPeerConnection + * @param {MediaStream} audioStream Stream to be used as second source + * (typically for audio) for localVideo and to be added as stream to the + * RTCPeerConnection + */ +function WebRtcPeer(mode, options, callback) { + if (!(this instanceof WebRtcPeer)) { + return new WebRtcPeer(mode, options, callback) + } + + WebRtcPeer.super_.call(this) + + if (options instanceof Function) { + callback = options + options = undefined + } + + options = options || {} + callback = (callback || noop).bind(this) + + var self = this + var localVideo = options.localVideo + var remoteVideo = options.remoteVideo + var videoStream = options.videoStream + var audioStream = options.audioStream + var mediaConstraints = options.mediaConstraints + + var connectionConstraints = options.connectionConstraints + var pc = options.peerConnection + var sendSource = options.sendSource || 'webcam' + + var dataChannelConfig = options.dataChannelConfig + var useDataChannels = options.dataChannels || false + var dataChannel + + var guid = uuid.v4() + var configuration = recursive({ + iceServers: freeice() + }, + options.configuration) + + var onicecandidate = options.onicecandidate + if (onicecandidate) this.on('icecandidate', onicecandidate) + + var oncandidategatheringdone = options.oncandidategatheringdone + if (oncandidategatheringdone) { + this.on('candidategatheringdone', oncandidategatheringdone) + } + + var simulcast = options.simulcast + var multistream = options.multistream + var interop = new sdpTranslator.Interop() + var candidatesQueueOut = [] + var candidategatheringdone = false + + Object.defineProperties(this, { + 'peerConnection': { + get: function () { + return pc + } + }, + + 'id': { + value: options.id || guid, + writable: false + }, + + 'remoteVideo': { + get: function () { + return remoteVideo + } + }, + + 'localVideo': { + get: function () { + return localVideo + } + }, + + 'dataChannel': { + get: function () { + return dataChannel + } + }, + + /** + * @member {(external:ImageData|undefined)} currentFrame + */ + 'currentFrame': { + get: function () { + // [ToDo] Find solution when we have a remote stream but we didn't set + // a remoteVideo tag + if (!remoteVideo) return; + + if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA) + throw new Error('No video stream data available') + + var canvas = document.createElement('canvas') + canvas.width = remoteVideo.videoWidth + canvas.height = remoteVideo.videoHeight + + canvas.getContext('2d').drawImage(remoteVideo, 0, 0) + + return canvas + } + } + }) + + // Init PeerConnection + if (!pc) { + pc = new RTCPeerConnection(configuration); + if (useDataChannels && !dataChannel) { + var dcId = 'WebRtcPeer-' + self.id + var dcOptions = undefined + if (dataChannelConfig) { + dcId = dataChannelConfig.id || dcId + dcOptions = dataChannelConfig.options + } + dataChannel = pc.createDataChannel(dcId, dcOptions); + if (dataChannelConfig) { + dataChannel.onopen = dataChannelConfig.onopen; + dataChannel.onclose = dataChannelConfig.onclose; + dataChannel.onmessage = dataChannelConfig.onmessage; + dataChannel.onbufferedamountlow = dataChannelConfig.onbufferedamountlow; + dataChannel.onerror = dataChannelConfig.onerror || noop; + } + } + } + + pc.addEventListener('icecandidate', function (event) { + var candidate = event.candidate + + if (EventEmitter.listenerCount(self, 'icecandidate') || + EventEmitter.listenerCount( + self, 'candidategatheringdone')) { + if (candidate) { + var cand + + if (multistream && usePlanB) { + cand = interop.candidateToUnifiedPlan(candidate) + } else { + cand = candidate + } + + self.emit('icecandidate', cand) + candidategatheringdone = false + } else if (!candidategatheringdone) { + self.emit('candidategatheringdone') + candidategatheringdone = true + } + } else if (!candidategatheringdone) { + // Not listening to 'icecandidate' or 'candidategatheringdone' events, queue + // the candidate until one of them is listened + candidatesQueueOut.push(candidate) + + if (!candidate) candidategatheringdone = true + } + }) + + pc.onaddstream = options.onaddstream + pc.onnegotiationneeded = options.onnegotiationneeded + this.on('newListener', function (event, listener) { + if (event === 'icecandidate' || event === 'candidategatheringdone') { + while (candidatesQueueOut.length) { + var candidate = candidatesQueueOut.shift() + + if (!candidate === (event === 'candidategatheringdone')) { + listener(candidate) + } + } + } + }) + + var addIceCandidate = bufferizeCandidates(pc) + + /** + * Callback function invoked when an ICE candidate is received. Developers are + * expected to invoke this function in order to complete the SDP negotiation. + * + * @function module:kurentoUtils.WebRtcPeer.prototype.addIceCandidate + * + * @param iceCandidate - Literal object with the ICE candidate description + * @param callback - Called when the ICE candidate has been added. + */ + this.addIceCandidate = function (iceCandidate, callback) { + var candidate + + if (multistream && usePlanB) { + candidate = interop.candidateToPlanB(iceCandidate) + } else { + candidate = new RTCIceCandidate(iceCandidate) + } + + console.log('ICE candidate received') + callback = (callback || noop).bind(this) + addIceCandidate(candidate, callback) + } + + this.generateOffer = function (callback) { + callback = callback.bind(this) + + var offerAudio = true + var offerVideo = true + // Constraints must have both blocks + if (mediaConstraints) { + offerAudio = (typeof mediaConstraints.audio === 'boolean') ? + mediaConstraints.audio : true + offerVideo = (typeof mediaConstraints.video === 'boolean') ? + mediaConstraints.video : true + } + + var browserDependantConstraints = (browser.name === 'Firefox' && + browser.version > 34) ? { + offerToReceiveAudio: (mode !== 'sendonly' && offerAudio), + offerToReceiveVideo: (mode !== 'sendonly' && offerVideo) + } : { + mandatory: { + OfferToReceiveAudio: (mode !== 'sendonly' && offerAudio), + OfferToReceiveVideo: (mode !== 'sendonly' && offerVideo) + }, + optional: [{ + DtlsSrtpKeyAgreement: true + }] + } + var constraints = recursive(browserDependantConstraints, + connectionConstraints) + + console.log('constraints: ' + JSON.stringify(constraints)) + + pc.createOffer(constraints).then(function (offer) { + console.log('Created SDP offer') + offer = mangleSdpToAddSimulcast(offer) + return pc.setLocalDescription(offer) + }).then(function () { + var localDescription = pc.localDescription + console.log('Local description set', localDescription.sdp) + if (multistream && usePlanB) { + localDescription = interop.toUnifiedPlan(localDescription) + console.log('offer::origPlanB->UnifiedPlan', dumpSDP( + localDescription)) + } + callback(null, localDescription.sdp, self.processAnswer.bind( + self)) + }).catch(callback) + } + + this.getLocalSessionDescriptor = function () { + return pc.localDescription + } + + this.getRemoteSessionDescriptor = function () { + return pc.remoteDescription + } + + function setRemoteVideo() { + if (remoteVideo) { + var stream = pc.getRemoteStreams()[0] + var url = stream ? URL.createObjectURL(stream) : '' + + remoteVideo.pause() + remoteVideo.src = url + remoteVideo.load() + + console.log('Remote URL:', url) + } + } + + this.showLocalVideo = function () { + localVideo.src = URL.createObjectURL(videoStream) + localVideo.muted = true + } + + this.send = function (data) { + if (dataChannel && dataChannel.readyState === 'open') { + dataChannel.send(data) + } else { + console.warn( + 'Trying to send data over a non-existing or closed data channel') + } + } + + /** + * Callback function invoked when a SDP answer is received. Developers are + * expected to invoke this function in order to complete the SDP negotiation. + * + * @function module:kurentoUtils.WebRtcPeer.prototype.processAnswer + * + * @param sdpAnswer - Description of sdpAnswer + * @param callback - + * Invoked after the SDP answer is processed, or there is an error. + */ + this.processAnswer = function (sdpAnswer, callback) { + callback = (callback || noop).bind(this) + + var answer = new RTCSessionDescription({ + type: 'answer', + sdp: sdpAnswer + }) + + if (multistream && usePlanB) { + var planBAnswer = interop.toPlanB(answer) + console.log('asnwer::planB', dumpSDP(planBAnswer)) + answer = planBAnswer + } + + console.log('SDP answer received, setting remote description') + + if (pc.signalingState === 'closed') { + return callback('PeerConnection is closed') + } + + pc.setRemoteDescription(answer, function () { + setRemoteVideo() + + callback() + }, + callback) + } + + /** + * Callback function invoked when a SDP offer is received. Developers are + * expected to invoke this function in order to complete the SDP negotiation. + * + * @function module:kurentoUtils.WebRtcPeer.prototype.processOffer + * + * @param sdpOffer - Description of sdpOffer + * @param callback - Called when the remote description has been set + * successfully. + */ + this.processOffer = function (sdpOffer, callback) { + callback = callback.bind(this) + + var offer = new RTCSessionDescription({ + type: 'offer', + sdp: sdpOffer + }) + + if (multistream && usePlanB) { + var planBOffer = interop.toPlanB(offer) + console.log('offer::planB', dumpSDP(planBOffer)) + offer = planBOffer + } + + console.log('SDP offer received, setting remote description') + + if (pc.signalingState === 'closed') { + return callback('PeerConnection is closed') + } + + pc.setRemoteDescription(offer).then(function () { + return setRemoteVideo() + }).then(function () { + return pc.createAnswer() + }).then(function (answer) { + answer = mangleSdpToAddSimulcast(answer) + console.log('Created SDP answer') + return pc.setLocalDescription(answer) + }).then(function () { + var localDescription = pc.localDescription + if (multistream && usePlanB) { + localDescription = interop.toUnifiedPlan(localDescription) + console.log('answer::origPlanB->UnifiedPlan', dumpSDP( + localDescription)) + } + console.log('Local description set', localDescription.sdp) + callback(null, localDescription.sdp) + }).catch(callback) + } + + function mangleSdpToAddSimulcast(answer) { + if (simulcast) { + if (browser.name === 'Chrome' || browser.name === 'Chromium') { + console.log('Adding multicast info') + answer = new RTCSessionDescription({ + 'type': answer.type, + 'sdp': removeFIDFromOffer(answer.sdp) + getSimulcastInfo( + videoStream) + }) + } else { + console.warn('Simulcast is only available in Chrome browser.') + } + } + + return answer + } + + /** + * This function creates the RTCPeerConnection object taking into account the + * properties received in the constructor. It starts the SDP negotiation + * process: generates the SDP offer and invokes the onsdpoffer callback. This + * callback is expected to send the SDP offer, in order to obtain an SDP + * answer from another peer. + */ + function start() { + if (pc.signalingState === 'closed') { + callback( + 'The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue' + ) + } + + if (videoStream && localVideo) { + self.showLocalVideo() + } + + if (videoStream) { + pc.addStream(videoStream) + } + + if (audioStream) { + pc.addStream(audioStream) + } + + // [Hack] https://code.google.com/p/chromium/issues/detail?id=443558 + var browser = parser.getBrowser() + if (mode === 'sendonly' && + (browser.name === 'Chrome' || browser.name === 'Chromium') && + browser.major === 39) { + mode = 'sendrecv' + } + + callback() + } + + if (mode !== 'recvonly' && !videoStream && !audioStream) { + function getMedia(constraints) { + if (constraints === undefined) { + constraints = MEDIA_CONSTRAINTS + } + getUserMedia(constraints, function (stream) { + videoStream = stream + start() + }, callback) + } + if (sendSource === 'webcam') { + getMedia(mediaConstraints) + } else { + getScreenConstraints(sendSource, function (error, constraints_) { + if (error) + return callback(error) + + constraints = [mediaConstraints] + constraints.unshift(constraints_) + getMedia(recursive.apply(undefined, constraints)) + }, guid) + } + } else { + setTimeout(start, 0) + } + + this.on('_dispose', function () { + if (localVideo) { + localVideo.pause() + localVideo.src = '' + localVideo.load() + //Unmute local video in case the video tag is later used for remote video + localVideo.muted = false + } + if (remoteVideo) { + remoteVideo.pause() + remoteVideo.src = '' + remoteVideo.load() + } + self.removeAllListeners() + + if (window.cancelChooseDesktopMedia !== undefined) { + window.cancelChooseDesktopMedia(guid) + } + }) +} +inherits(WebRtcPeer, EventEmitter) + +function createEnableDescriptor(type) { + var method = 'get' + type + 'Tracks' + + return { + enumerable: true, + get: function () { + // [ToDo] Should return undefined if not all tracks have the same value? + + if (!this.peerConnection) return + + var streams = this.peerConnection.getLocalStreams() + if (!streams.length) return + + for (var i = 0, stream; stream = streams[i]; i++) { + var tracks = stream[method]() + for (var j = 0, track; track = tracks[j]; j++) + if (!track.enabled) return false + } + + return true + }, + set: function (value) { + function trackSetEnable(track) { + track.enabled = value + } + + this.peerConnection.getLocalStreams().forEach(function (stream) { + stream[method]().forEach(trackSetEnable) + }) + } + } +} + +Object.defineProperties(WebRtcPeer.prototype, { + 'enabled': { + enumerable: true, + get: function () { + return this.audioEnabled && this.videoEnabled + }, + set: function (value) { + this.audioEnabled = this.videoEnabled = value + } + }, + 'audioEnabled': createEnableDescriptor('Audio'), + 'videoEnabled': createEnableDescriptor('Video') +}) + +WebRtcPeer.prototype.getLocalStream = function (index) { + if (this.peerConnection) { + return this.peerConnection.getLocalStreams()[index || 0] + } +} + +WebRtcPeer.prototype.getRemoteStream = function (index) { + if (this.peerConnection) { + return this.peerConnection.getRemoteStreams()[index || 0] + } +} + +/** + * @description This method frees the resources used by WebRtcPeer. + * + * @function module:kurentoUtils.WebRtcPeer.prototype.dispose + */ +WebRtcPeer.prototype.dispose = function () { + console.log('Disposing WebRtcPeer') + + var pc = this.peerConnection + var dc = this.dataChannel + try { + if (dc) { + if (dc.signalingState === 'closed') return + + dc.close() + } + + if (pc) { + if (pc.signalingState === 'closed') return + + pc.getLocalStreams().forEach(streamStop) + + // FIXME This is not yet implemented in firefox + // if(videoStream) pc.removeStream(videoStream); + // if(audioStream) pc.removeStream(audioStream); + + pc.close() + } + } catch (err) { + console.warn('Exception disposing webrtc peer ' + err) + } + + this.emit('_dispose') +} + +// +// Specialized child classes +// + +function WebRtcPeerRecvonly(options, callback) { + if (!(this instanceof WebRtcPeerRecvonly)) { + return new WebRtcPeerRecvonly(options, callback) + } + + WebRtcPeerRecvonly.super_.call(this, 'recvonly', options, callback) +} +inherits(WebRtcPeerRecvonly, WebRtcPeer) + +function WebRtcPeerSendonly(options, callback) { + if (!(this instanceof WebRtcPeerSendonly)) { + return new WebRtcPeerSendonly(options, callback) + } + + WebRtcPeerSendonly.super_.call(this, 'sendonly', options, callback) +} +inherits(WebRtcPeerSendonly, WebRtcPeer) + +function WebRtcPeerSendrecv(options, callback) { + if (!(this instanceof WebRtcPeerSendrecv)) { + return new WebRtcPeerSendrecv(options, callback) + } + + WebRtcPeerSendrecv.super_.call(this, 'sendrecv', options, callback) +} +inherits(WebRtcPeerSendrecv, WebRtcPeer) + +function harkUtils(stream, options) { + return hark(stream, options); +} + +exports.bufferizeCandidates = bufferizeCandidates + +exports.WebRtcPeerRecvonly = WebRtcPeerRecvonly +exports.WebRtcPeerSendonly = WebRtcPeerSendonly +exports.WebRtcPeerSendrecv = WebRtcPeerSendrecv +exports.hark = harkUtils + +},{"events":109,"freeice":3,"hark":6,"inherits":7,"kurento-browser-extensions":undefined,"merge":20,"sdp-translator":30,"ua-parser-js":87,"uuid":91}],19:[function(require,module,exports){ +/* + * (C) Copyright 2014 Kurento (http://kurento.org/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * This module contains a set of reusable components that have been found useful + * during the development of the WebRTC applications with Kurento. + * + * @module kurentoUtils + * + * @copyright 2014 Kurento (http://kurento.org/) + * @license ALv2 + */ + +var WebRtcPeer = require('./WebRtcPeer'); + +exports.WebRtcPeer = WebRtcPeer; + +},{"./WebRtcPeer":18}],20:[function(require,module,exports){ +/*! + * @name JavaScript/NodeJS Merge v1.2.0 + * @author yeikos + * @repository https://github.com/yeikos/js.merge + + * Copyright 2014 yeikos - MIT license + * https://raw.github.com/yeikos/js.merge/master/LICENSE + */ + +;(function(isNode) { + + /** + * Merge one or more objects + * @param bool? clone + * @param mixed,... arguments + * @return object + */ + + var Public = function(clone) { + + return merge(clone === true, false, arguments); + + }, publicName = 'merge'; + + /** + * Merge two or more objects recursively + * @param bool? clone + * @param mixed,... arguments + * @return object + */ + + Public.recursive = function(clone) { + + return merge(clone === true, true, arguments); + + }; + + /** + * Clone the input removing any reference + * @param mixed input + * @return mixed + */ + + Public.clone = function(input) { + + var output = input, + type = typeOf(input), + index, size; + + if (type === 'array') { + + output = []; + size = input.length; + + for (index=0;index 10000) return; + var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(str); + if (!match) return; + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function short(ms) { + if (ms >= d) return Math.round(ms / d) + 'd'; + if (ms >= h) return Math.round(ms / h) + 'h'; + if (ms >= m) return Math.round(ms / m) + 'm'; + if (ms >= s) return Math.round(ms / s) + 's'; + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function long(ms) { + return plural(ms, d, 'day') + || plural(ms, h, 'hour') + || plural(ms, m, 'minute') + || plural(ms, s, 'second') + || ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) return; + if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name; + return Math.ceil(ms / n) + ' ' + name + 's'; +} + +},{}],22:[function(require,module,exports){ +/** + # normalice + + Normalize an ice server configuration object (or plain old string) into a format + that is usable in all browsers supporting WebRTC. Primarily this module is designed + to help with the transition of the `url` attribute of the configuration object to + the `urls` attribute. + + ## Example Usage + + <<< examples/simple.js + +**/ + +var protocols = [ + 'stun:', + 'turn:' +]; + +module.exports = function(input) { + var url = (input || {}).url || input; + var protocol; + var parts; + var output = {}; + + // if we don't have a string url, then allow the input to passthrough + if (typeof url != 'string' && (! (url instanceof String))) { + return input; + } + + // trim the url string, and convert to an array + url = url.trim(); + + // if the protocol is not known, then passthrough + protocol = protocols[protocols.indexOf(url.slice(0, 5))]; + if (! protocol) { + return input; + } + + // now let's attack the remaining url parts + url = url.slice(5); + parts = url.split('@'); + + output.username = input.username; + output.credential = input.credential; + // if we have an authentication part, then set the credentials + if (parts.length > 1) { + url = parts[1]; + parts = parts[0].split(':'); + + // add the output credential and username + output.username = parts[0]; + output.credential = (input || {}).credential || parts[1] || ''; + } + + output.url = protocol + url; + output.urls = [ output.url ]; + + return output; +}; + +},{}],23:[function(require,module,exports){ +'use strict'; + +var has = Object.prototype.hasOwnProperty; + +/** + * Simple query string parser. + * + * @param {String} query The query string that needs to be parsed. + * @returns {Object} + * @api public + */ +function querystring(query) { + var parser = /([^=?&]+)=?([^&]*)/g + , result = {} + , part; + + // + // Little nifty parsing hack, leverage the fact that RegExp.exec increments + // the lastIndex property so we can continue executing this loop until we've + // parsed all results. + // + for (; + part = parser.exec(query); + result[decodeURIComponent(part[1])] = decodeURIComponent(part[2]) + ); + + return result; +} + +/** + * Transform a query string to an object. + * + * @param {Object} obj Object that should be transformed. + * @param {String} prefix Optional prefix. + * @returns {String} + * @api public + */ +function querystringify(obj, prefix) { + prefix = prefix || ''; + + var pairs = []; + + // + // Optionally prefix with a '?' if needed + // + if ('string' !== typeof prefix) prefix = '?'; + + for (var key in obj) { + if (has.call(obj, key)) { + pairs.push(encodeURIComponent(key) +'='+ encodeURIComponent(obj[key])); + } + } + + return pairs.length ? prefix + pairs.join('&') : ''; +} + +// +// Expose the module. +// +exports.stringify = querystringify; +exports.parse = querystring; + +},{}],24:[function(require,module,exports){ +'use strict'; + +/** + * Check if we're required to add a port number. + * + * @see https://url.spec.whatwg.org/#default-port + * @param {Number|String} port Port number we need to check + * @param {String} protocol Protocol we need to check against. + * @returns {Boolean} Is it a default port for the given protocol + * @api private + */ +module.exports = function required(port, protocol) { + protocol = protocol.split(':')[0]; + port = +port; + + if (!port) return false; + + switch (protocol) { + case 'http': + case 'ws': + return port !== 80; + + case 'https': + case 'wss': + return port !== 443; + + case 'ftp': + return port !== 21; + + case 'gopher': + return port !== 70; + + case 'file': + return false; + } + + return port !== 0; +}; + +},{}],25:[function(require,module,exports){ +var grammar = module.exports = { + v: [{ + name: 'version', + reg: /^(\d*)$/ + }], + o: [{ //o=- 20518 0 IN IP4 203.0.113.1 + // NB: sessionId will be a String in most cases because it is huge + name: 'origin', + reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/, + names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'], + format: "%s %s %d %s IP%d %s" + }], + // default parsing of these only (though some of these feel outdated) + s: [{ name: 'name' }], + i: [{ name: 'description' }], + u: [{ name: 'uri' }], + e: [{ name: 'email' }], + p: [{ name: 'phone' }], + z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly.. + r: [{ name: 'repeats' }], // TODO: this one can also be parsed properly + //k: [{}], // outdated thing ignored + t: [{ //t=0 0 + name: 'timing', + reg: /^(\d*) (\d*)/, + names: ['start', 'stop'], + format: "%d %d" + }], + c: [{ //c=IN IP4 10.47.197.26 + name: 'connection', + reg: /^IN IP(\d) (\S*)/, + names: ['version', 'ip'], + format: "IN IP%d %s" + }], + b: [{ //b=AS:4000 + push: 'bandwidth', + reg: /^(TIAS|AS|CT|RR|RS):(\d*)/, + names: ['type', 'limit'], + format: "%s:%s" + }], + m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31 + // NB: special - pushes to session + // TODO: rtp/fmtp should be filtered by the payloads found here? + reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/, + names: ['type', 'port', 'protocol', 'payloads'], + format: "%s %d %s %s" + }], + a: [ + { //a=rtpmap:110 opus/48000/2 + push: 'rtp', + reg: /^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/, + names: ['payload', 'codec', 'rate', 'encoding'], + format: function (o) { + return (o.encoding) ? + "rtpmap:%d %s/%s/%s": + o.rate ? + "rtpmap:%d %s/%s": + "rtpmap:%d %s"; + } + }, + { + //a=fmtp:108 profile-level-id=24;object=23;bitrate=64000 + //a=fmtp:111 minptime=10; useinbandfec=1 + push: 'fmtp', + reg: /^fmtp:(\d*) ([\S| ]*)/, + names: ['payload', 'config'], + format: "fmtp:%d %s" + }, + { //a=control:streamid=0 + name: 'control', + reg: /^control:(.*)/, + format: "control:%s" + }, + { //a=rtcp:65179 IN IP4 193.84.77.194 + name: 'rtcp', + reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/, + names: ['port', 'netType', 'ipVer', 'address'], + format: function (o) { + return (o.address != null) ? + "rtcp:%d %s IP%d %s": + "rtcp:%d"; + } + }, + { //a=rtcp-fb:98 trr-int 100 + push: 'rtcpFbTrrInt', + reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/, + names: ['payload', 'value'], + format: "rtcp-fb:%d trr-int %d" + }, + { //a=rtcp-fb:98 nack rpsi + push: 'rtcpFb', + reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/, + names: ['payload', 'type', 'subtype'], + format: function (o) { + return (o.subtype != null) ? + "rtcp-fb:%s %s %s": + "rtcp-fb:%s %s"; + } + }, + { //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset + //a=extmap:1/recvonly URI-gps-string + push: 'ext', + reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/, + names: ['value', 'uri', 'config'], // value may include "/direction" suffix + format: function (o) { + return (o.config != null) ? + "extmap:%s %s %s": + "extmap:%s %s"; + } + }, + { + //a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 + push: 'crypto', + reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/, + names: ['id', 'suite', 'config', 'sessionConfig'], + format: function (o) { + return (o.sessionConfig != null) ? + "crypto:%d %s %s %s": + "crypto:%d %s %s"; + } + }, + { //a=setup:actpass + name: 'setup', + reg: /^setup:(\w*)/, + format: "setup:%s" + }, + { //a=mid:1 + name: 'mid', + reg: /^mid:([^\s]*)/, + format: "mid:%s" + }, + { //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a + name: 'msid', + reg: /^msid:(.*)/, + format: "msid:%s" + }, + { //a=ptime:20 + name: 'ptime', + reg: /^ptime:(\d*)/, + format: "ptime:%d" + }, + { //a=maxptime:60 + name: 'maxptime', + reg: /^maxptime:(\d*)/, + format: "maxptime:%d" + }, + { //a=sendrecv + name: 'direction', + reg: /^(sendrecv|recvonly|sendonly|inactive)/ + }, + { //a=ice-lite + name: 'icelite', + reg: /^(ice-lite)/ + }, + { //a=ice-ufrag:F7gI + name: 'iceUfrag', + reg: /^ice-ufrag:(\S*)/, + format: "ice-ufrag:%s" + }, + { //a=ice-pwd:x9cml/YzichV2+XlhiMu8g + name: 'icePwd', + reg: /^ice-pwd:(\S*)/, + format: "ice-pwd:%s" + }, + { //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33 + name: 'fingerprint', + reg: /^fingerprint:(\S*) (\S*)/, + names: ['type', 'hash'], + format: "fingerprint:%s %s" + }, + { + //a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host + //a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 + //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 + //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 + //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 + push:'candidates', + reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/, + names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'], + format: function (o) { + var str = "candidate:%s %d %s %d %s %d typ %s"; + + str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v"; + + // NB: candidate has three optional chunks, so %void middles one if it's missing + str += (o.tcptype != null) ? " tcptype %s" : "%v"; + + if (o.generation != null) { + str += " generation %d"; + } + return str; + } + }, + { //a=end-of-candidates (keep after the candidates line for readability) + name: 'endOfCandidates', + reg: /^(end-of-candidates)/ + }, + { //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ... + name: 'remoteCandidates', + reg: /^remote-candidates:(.*)/, + format: "remote-candidates:%s" + }, + { //a=ice-options:google-ice + name: 'iceOptions', + reg: /^ice-options:(\S*)/, + format: "ice-options:%s" + }, + { //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 + push: "ssrcs", + reg: /^ssrc:(\d*) ([\w_]*):(.*)/, + names: ['id', 'attribute', 'value'], + format: "ssrc:%d %s:%s" + }, + { //a=ssrc-group:FEC 1 2 + push: "ssrcGroups", + reg: /^ssrc-group:(\w*) (.*)/, + names: ['semantics', 'ssrcs'], + format: "ssrc-group:%s %s" + }, + { //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV + name: "msidSemantic", + reg: /^msid-semantic:\s?(\w*) (\S*)/, + names: ['semantic', 'token'], + format: "msid-semantic: %s %s" // space after ":" is not accidental + }, + { //a=group:BUNDLE audio video + push: 'groups', + reg: /^group:(\w*) (.*)/, + names: ['type', 'mids'], + format: "group:%s %s" + }, + { //a=rtcp-mux + name: 'rtcpMux', + reg: /^(rtcp-mux)/ + }, + { //a=rtcp-rsize + name: 'rtcpRsize', + reg: /^(rtcp-rsize)/ + }, + { // any a= that we don't understand is kepts verbatim on media.invalid + push: 'invalid', + names: ["value"] + } + ] +}; + +// set sensible defaults to avoid polluting the grammar with boring details +Object.keys(grammar).forEach(function (key) { + var objs = grammar[key]; + objs.forEach(function (obj) { + if (!obj.reg) { + obj.reg = /(.*)/; + } + if (!obj.format) { + obj.format = "%s"; + } + }); +}); + +},{}],26:[function(require,module,exports){ +var parser = require('./parser'); +var writer = require('./writer'); + +exports.write = writer; +exports.parse = parser.parse; +exports.parseFmtpConfig = parser.parseFmtpConfig; +exports.parsePayloads = parser.parsePayloads; +exports.parseRemoteCandidates = parser.parseRemoteCandidates; + +},{"./parser":27,"./writer":28}],27:[function(require,module,exports){ +var toIntIfInt = function (v) { + return String(Number(v)) === v ? Number(v) : v; +}; + +var attachProperties = function (match, location, names, rawName) { + if (rawName && !names) { + location[rawName] = toIntIfInt(match[1]); + } + else { + for (var i = 0; i < names.length; i += 1) { + if (match[i+1] != null) { + location[names[i]] = toIntIfInt(match[i+1]); + } + } + } +}; + +var parseReg = function (obj, location, content) { + var needsBlank = obj.name && obj.names; + if (obj.push && !location[obj.push]) { + location[obj.push] = []; + } + else if (needsBlank && !location[obj.name]) { + location[obj.name] = {}; + } + var keyLocation = obj.push ? + {} : // blank object that will be pushed + needsBlank ? location[obj.name] : location; // otherwise, named location or root + + attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name); + + if (obj.push) { + location[obj.push].push(keyLocation); + } +}; + +var grammar = require('./grammar'); +var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/); + +exports.parse = function (sdp) { + var session = {} + , media = [] + , location = session; // points at where properties go under (one of the above) + + // parse lines we understand + sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) { + var type = l[0]; + var content = l.slice(2); + if (type === 'm') { + media.push({rtp: [], fmtp: []}); + location = media[media.length-1]; // point at latest media line + } + + for (var j = 0; j < (grammar[type] || []).length; j += 1) { + var obj = grammar[type][j]; + if (obj.reg.test(content)) { + return parseReg(obj, location, content); + } + } + }); + + session.media = media; // link it up + return session; +}; + +var fmtpReducer = function (acc, expr) { + var s = expr.split('='); + if (s.length === 2) { + acc[s[0]] = toIntIfInt(s[1]); + } + return acc; +}; + +exports.parseFmtpConfig = function (str) { + return str.split(/\;\s?/).reduce(fmtpReducer, {}); +}; + +exports.parsePayloads = function (str) { + return str.split(' ').map(Number); +}; + +exports.parseRemoteCandidates = function (str) { + var candidates = []; + var parts = str.split(' ').map(toIntIfInt); + for (var i = 0; i < parts.length; i += 3) { + candidates.push({ + component: parts[i], + ip: parts[i + 1], + port: parts[i + 2] + }); + } + return candidates; +}; + +},{"./grammar":25}],28:[function(require,module,exports){ +var grammar = require('./grammar'); + +// customized util.format - discards excess arguments and can void middle ones +var formatRegExp = /%[sdv%]/g; +var format = function (formatStr) { + var i = 1; + var args = arguments; + var len = args.length; + return formatStr.replace(formatRegExp, function (x) { + if (i >= len) { + return x; // missing argument + } + var arg = args[i]; + i += 1; + switch (x) { + case '%%': + return '%'; + case '%s': + return String(arg); + case '%d': + return Number(arg); + case '%v': + return ''; + } + }); + // NB: we discard excess arguments - they are typically undefined from makeLine +}; + +var makeLine = function (type, obj, location) { + var str = obj.format instanceof Function ? + (obj.format(obj.push ? location : location[obj.name])) : + obj.format; + + var args = [type + '=' + str]; + if (obj.names) { + for (var i = 0; i < obj.names.length; i += 1) { + var n = obj.names[i]; + if (obj.name) { + args.push(location[obj.name][n]); + } + else { // for mLine and push attributes + args.push(location[obj.names[i]]); + } + } + } + else { + args.push(location[obj.name]); + } + return format.apply(null, args); +}; + +// RFC specified order +// TODO: extend this with all the rest +var defaultOuterOrder = [ + 'v', 'o', 's', 'i', + 'u', 'e', 'p', 'c', + 'b', 't', 'r', 'z', 'a' +]; +var defaultInnerOrder = ['i', 'c', 'b', 'a']; + + +module.exports = function (session, opts) { + opts = opts || {}; + // ensure certain properties exist + if (session.version == null) { + session.version = 0; // "v=0" must be there (only defined version atm) + } + if (session.name == null) { + session.name = " "; // "s= " must be there if no meaningful name set + } + session.media.forEach(function (mLine) { + if (mLine.payloads == null) { + mLine.payloads = ""; + } + }); + + var outerOrder = opts.outerOrder || defaultOuterOrder; + var innerOrder = opts.innerOrder || defaultInnerOrder; + var sdp = []; + + // loop through outerOrder for matching properties on session + outerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in session && session[obj.name] != null) { + sdp.push(makeLine(type, obj, session)); + } + else if (obj.push in session && session[obj.push] != null) { + session[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + + // then for each media line, follow the innerOrder + session.media.forEach(function (mLine) { + sdp.push(makeLine('m', grammar.m[0], mLine)); + + innerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in mLine && mLine[obj.name] != null) { + sdp.push(makeLine(type, obj, mLine)); + } + else if (obj.push in mLine && mLine[obj.push] != null) { + mLine[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + }); + + return sdp.join('\r\n') + '\r\n'; +}; + +},{"./grammar":25}],29:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = function arrayEquals(array) { + // if the other array is a falsy value, return + if (!array) + return false; + + // compare lengths - can save a lot of time + if (this.length != array.length) + return false; + + for (var i = 0, l = this.length; i < l; i++) { + // Check if we have nested arrays + if (this[i] instanceof Array && array[i] instanceof Array) { + // recurse into the nested arrays + if (!arrayEquals.apply(this[i], [array[i]])) + return false; + } else if (this[i] != array[i]) { + // Warning - two different object instances will never be equal: + // {x:20} != {x:20} + return false; + } + } + return true; +}; + + +},{}],30:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.Interop = require('./interop'); + +},{"./interop":31}],31:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* global RTCSessionDescription */ +/* global RTCIceCandidate */ +/* jshint -W097 */ +"use strict"; + +var transform = require('./transform'); +var arrayEquals = require('./array-equals'); + +function Interop() { + + /** + * This map holds the most recent Unified Plan offer/answer SDP that was + * converted to Plan B, with the SDP type ('offer' or 'answer') as keys and + * the SDP string as values. + * + * @type {{}} + */ + this.cache = { + mlB2UMap : {}, + mlU2BMap : {} + }; +} + +module.exports = Interop; + +/** + * Changes the candidate args to match with the related Unified Plan + */ +Interop.prototype.candidateToUnifiedPlan = function(candidate) { + var cand = new RTCIceCandidate(candidate); + + cand.sdpMLineIndex = this.cache.mlB2UMap[cand.sdpMLineIndex]; + /* TODO: change sdpMid to (audio|video)-SSRC */ + + return cand; +}; + +/** + * Changes the candidate args to match with the related Plan B + */ +Interop.prototype.candidateToPlanB = function(candidate) { + var cand = new RTCIceCandidate(candidate); + + if (cand.sdpMid.indexOf('audio') === 0) { + cand.sdpMid = 'audio'; + } else if (cand.sdpMid.indexOf('video') === 0) { + cand.sdpMid = 'video'; + } else { + throw new Error('candidate with ' + cand.sdpMid + ' not allowed'); + } + + cand.sdpMLineIndex = this.cache.mlU2BMap[cand.sdpMLineIndex]; + + return cand; +}; + +/** + * Returns the index of the first m-line with the given media type and with a + * direction which allows sending, in the last Unified Plan description with + * type "answer" converted to Plan B. Returns {null} if there is no saved + * answer, or if none of its m-lines with the given type allow sending. + * @param type the media type ("audio" or "video"). + * @returns {*} + */ +Interop.prototype.getFirstSendingIndexFromAnswer = function(type) { + if (!this.cache.answer) { + return null; + } + + var session = transform.parse(this.cache.answer); + if (session && session.media && Array.isArray(session.media)){ + for (var i = 0; i < session.media.length; i++) { + if (session.media[i].type == type && + (!session.media[i].direction /* default to sendrecv */ || + session.media[i].direction === 'sendrecv' || + session.media[i].direction === 'sendonly')){ + return i; + } + } + } + + return null; +}; + +/** + * This method transforms a Unified Plan SDP to an equivalent Plan B SDP. A + * PeerConnection wrapper transforms the SDP to Plan B before passing it to the + * application. + * + * @param desc + * @returns {*} + */ +Interop.prototype.toPlanB = function(desc) { + var self = this; + //#region Preliminary input validation. + + if (typeof desc !== 'object' || desc === null || + typeof desc.sdp !== 'string') { + console.warn('An empty description was passed as an argument.'); + return desc; + } + + // Objectify the SDP for easier manipulation. + var session = transform.parse(desc.sdp); + + // If the SDP contains no media, there's nothing to transform. + if (typeof session.media === 'undefined' || + !Array.isArray(session.media) || session.media.length === 0) { + console.warn('The description has no media.'); + return desc; + } + + // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B + // SDP has a video, an audio and a data "channel" at most. + if (session.media.length <= 3 && session.media.every(function(m) { + return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; + })) { + console.warn('This description does not look like Unified Plan.'); + return desc; + } + + //#endregion + + // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443 + var sdp = desc.sdp; + var rewrite = false; + for (var i = 0; i < session.media.length; i++) { + var uLine = session.media[i]; + uLine.rtp.forEach(function(rtp) { + if (rtp.codec === 'NULL') + { + rewrite = true; + var offer = transform.parse(self.cache.offer); + rtp.codec = offer.media[i].rtp[0].codec; + } + }); + } + if (rewrite) { + sdp = transform.write(session); + } + + // Unified Plan SDP is our "precious". Cache it for later use in the Plan B + // -> Unified Plan transformation. + this.cache[desc.type] = sdp; + + //#region Convert from Unified Plan to Plan B. + + // We rebuild the session.media array. + var media = session.media; + session.media = []; + + // Associative array that maps channel types to channel objects for fast + // access to channel objects by their type, e.g. type2bl['audio']->channel + // obj. + var type2bl = {}; + + // Used to build the group:BUNDLE value after the channels construction + // loop. + var types = []; + + media.forEach(function(uLine) { + // rtcp-mux is required in the Plan B SDP. + if ((typeof uLine.rtcpMux !== 'string' || + uLine.rtcpMux !== 'rtcp-mux') && + uLine.direction !== 'inactive') { + throw new Error('Cannot convert to Plan B because m-lines ' + + 'without the rtcp-mux attribute were found.'); + } + + // If we don't have a channel for this uLine.type OR the selected is + // inactive, then select this uLine as the channel basis. + if (typeof type2bl[uLine.type] === 'undefined' || + type2bl[uLine.type].direction === 'inactive') { + type2bl[uLine.type] = uLine; + } + + if (uLine.protocol != type2bl[uLine.type].protocol) { + throw new Error('Cannot convert to Plan B because m-lines ' + + 'have different protocols and this library does not have ' + + 'support for that'); + } + + if (uLine.payloads != type2bl[uLine.type].payloads) { + throw new Error('Cannot convert to Plan B because m-lines ' + + 'have different payloads and this library does not have ' + + 'support for that'); + } + + }); + + // Implode the Unified Plan m-lines/tracks into Plan B channels. + media.forEach(function(uLine) { + if (uLine.type === 'application') { + session.media.push(uLine); + types.push(uLine.mid); + return; + } + + // Add sources to the channel and handle a=msid. + if (typeof uLine.sources === 'object') { + Object.keys(uLine.sources).forEach(function(ssrc) { + if (typeof type2bl[uLine.type].sources !== 'object') + type2bl[uLine.type].sources = {}; + + // Assign the sources to the channel. + type2bl[uLine.type].sources[ssrc] = + uLine.sources[ssrc]; + + if (typeof uLine.msid !== 'undefined') { + // In Plan B the msid is an SSRC attribute. Also, we don't + // care about the obsolete label and mslabel attributes. + // + // Note that it is not guaranteed that the uLine will + // have an msid. recvonly channels in particular don't have + // one. + type2bl[uLine.type].sources[ssrc].msid = + uLine.msid; + } + // NOTE ssrcs in ssrc groups will share msids, as + // draft-uberti-rtcweb-plan-00 mandates. + }); + } + + // Add ssrc groups to the channel. + if (typeof uLine.ssrcGroups !== 'undefined' && + Array.isArray(uLine.ssrcGroups)) { + + // Create the ssrcGroups array, if it's not defined. + if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' || + !Array.isArray(type2bl[uLine.type].ssrcGroups)) { + type2bl[uLine.type].ssrcGroups = []; + } + + type2bl[uLine.type].ssrcGroups = + type2bl[uLine.type].ssrcGroups.concat( + uLine.ssrcGroups); + } + + if (type2bl[uLine.type] === uLine) { + // Plan B mids are in ['audio', 'video', 'data'] + uLine.mid = uLine.type; + + // Plan B doesn't support/need the bundle-only attribute. + delete uLine.bundleOnly; + + // In Plan B the msid is an SSRC attribute. + delete uLine.msid; + + if (uLine.type == media[0].type) { + types.unshift(uLine.type); + // Add the channel to the new media array. + session.media.unshift(uLine); + } else { + types.push(uLine.type); + // Add the channel to the new media array. + session.media.push(uLine); + } + } + }); + + if (typeof session.groups !== 'undefined') { + // We regenerate the BUNDLE group with the new mids. + session.groups.some(function(group) { + if (group.type === 'BUNDLE') { + group.mids = types.join(' '); + return true; + } + }); + } + + // msid semantic + session.msidSemantic = { + semantic: 'WMS', + token: '*' + }; + + var resStr = transform.write(session); + + return new RTCSessionDescription({ + type: desc.type, + sdp: resStr + }); + + //#endregion +}; + +/* follow rules defined in RFC4145 */ +function addSetupAttr(uLine) { + if (typeof uLine.setup === 'undefined') { + return; + } + + if (uLine.setup === "active") { + uLine.setup = "passive"; + } else if (uLine.setup === "passive") { + uLine.setup = "active"; + } +} + +/** + * This method transforms a Plan B SDP to an equivalent Unified Plan SDP. A + * PeerConnection wrapper transforms the SDP to Unified Plan before passing it + * to FF. + * + * @param desc + * @returns {*} + */ +Interop.prototype.toUnifiedPlan = function(desc) { + var self = this; + //#region Preliminary input validation. + + if (typeof desc !== 'object' || desc === null || + typeof desc.sdp !== 'string') { + console.warn('An empty description was passed as an argument.'); + return desc; + } + + var session = transform.parse(desc.sdp); + + // If the SDP contains no media, there's nothing to transform. + if (typeof session.media === 'undefined' || + !Array.isArray(session.media) || session.media.length === 0) { + console.warn('The description has no media.'); + return desc; + } + + // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has + // a video, an audio and a data "channel" at most. + if (session.media.length > 3 || !session.media.every(function(m) { + return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; + })) { + console.warn('This description does not look like Plan B.'); + return desc; + } + + // Make sure this Plan B SDP can be converted to a Unified Plan SDP. + var mids = []; + session.media.forEach(function(m) { + mids.push(m.mid); + }); + + var hasBundle = false; + if (typeof session.groups !== 'undefined' && + Array.isArray(session.groups)) { + hasBundle = session.groups.every(function(g) { + return g.type !== 'BUNDLE' || + arrayEquals.apply(g.mids.sort(), [mids.sort()]); + }); + } + + if (!hasBundle) { + var mustBeBundle = false; + + session.media.forEach(function(m) { + if (m.direction !== 'inactive') { + mustBeBundle = true; + } + }); + + if (mustBeBundle) { + throw new Error("Cannot convert to Unified Plan because m-lines that" + + " are not bundled were found."); + } + } + + //#endregion + + + //#region Convert from Plan B to Unified Plan. + + // Unfortunately, a Plan B offer/answer doesn't have enough information to + // rebuild an equivalent Unified Plan offer/answer. + // + // For example, if this is a local answer (in Unified Plan style) that we + // convert to Plan B prior to handing it over to the application (the + // PeerConnection wrapper called us, for instance, after a successful + // createAnswer), we want to remember the m-line at which we've seen the + // (local) SSRC. That's because when the application wants to do call the + // SLD method, forcing us to do the inverse transformation (from Plan B to + // Unified Plan), we need to know to which m-line to assign the (local) + // SSRC. We also need to know all the other m-lines that the original + // answer had and include them in the transformed answer as well. + // + // Another example is if this is a remote offer that we convert to Plan B + // prior to giving it to the application, we want to remember the mid at + // which we've seen the (remote) SSRC. + // + // In the iteration that follows, we use the cached Unified Plan (if it + // exists) to assign mids to ssrcs. + + var type; + if (desc.type === 'answer') { + type = 'offer'; + } else if (desc.type === 'offer') { + type = 'answer'; + } else { + throw new Error("Type '" + desc.type + "' not supported."); + } + + var cached; + if (typeof this.cache[type] !== 'undefined') { + cached = transform.parse(this.cache[type]); + } + + var recvonlySsrcs = { + audio: {}, + video: {} + }; + + // A helper map that sends mids to m-line objects. We use it later to + // rebuild the Unified Plan style session.media array. + var mid2ul = {}; + var bIdx = 0; + var uIdx = 0; + + var sources2ul = {}; + + var candidates; + var iceUfrag; + var icePwd; + var fingerprint; + var payloads = {}; + var rtcpFb = {}; + var rtp = {}; + + session.media.forEach(function(bLine) { + if ((typeof bLine.rtcpMux !== 'string' || + bLine.rtcpMux !== 'rtcp-mux') && + bLine.direction !== 'inactive') { + throw new Error("Cannot convert to Unified Plan because m-lines " + + "without the rtcp-mux attribute were found."); + } + + if (bLine.type === 'application') { + mid2ul[bLine.mid] = bLine; + return; + } + + // With rtcp-mux and bundle all the channels should have the same ICE + // stuff. + var sources = bLine.sources; + var ssrcGroups = bLine.ssrcGroups; + var port = bLine.port; + + /* Chrome adds different candidates even using bundle, so we concat the candidates list */ + if (typeof bLine.candidates != 'undefined') { + if (typeof candidates != 'undefined') { + candidates = candidates.concat(bLine.candidates); + } else { + candidates = bLine.candidates; + } + } + + if ((typeof iceUfrag != 'undefined') && (typeof bLine.iceUfrag != 'undefined') && (iceUfrag != bLine.iceUfrag)) { + throw new Error("Only BUNDLE supported, iceUfrag must be the same for all m-lines.\n" + + "\tLast iceUfrag: " + iceUfrag + "\n" + + "\tNew iceUfrag: " + bLine.iceUfrag + ); + } + + if (typeof bLine.iceUfrag != 'undefined') { + iceUfrag = bLine.iceUfrag; + } + + if ((typeof icePwd != 'undefined') && (typeof bLine.icePwd != 'undefined') && (icePwd != bLine.icePwd)) { + throw new Error("Only BUNDLE supported, icePwd must be the same for all m-lines.\n" + + "\tLast icePwd: " + icePwd + "\n" + + "\tNew icePwd: " + bLine.icePwd + ); + } + + if (typeof bLine.icePwd != 'undefined') { + icePwd = bLine.icePwd; + } + + if ((typeof fingerprint != 'undefined') && (typeof bLine.fingerprint != 'undefined') && + (fingerprint.type != bLine.fingerprint.type || fingerprint.hash != bLine.fingerprint.hash)) { + throw new Error("Only BUNDLE supported, fingerprint must be the same for all m-lines.\n" + + "\tLast fingerprint: " + JSON.stringify(fingerprint) + "\n" + + "\tNew fingerprint: " + JSON.stringify(bLine.fingerprint) + ); + } + + if (typeof bLine.fingerprint != 'undefined') { + fingerprint = bLine.fingerprint; + } + + payloads[bLine.type] = bLine.payloads; + rtcpFb[bLine.type] = bLine.rtcpFb; + rtp[bLine.type] = bLine.rtp; + + // inverted ssrc group map + var ssrc2group = {}; + if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) { + ssrcGroups.forEach(function (ssrcGroup) { + // XXX This might brake if an SSRC is in more than one group + // for some reason. + if (typeof ssrcGroup.ssrcs !== 'undefined' && + Array.isArray(ssrcGroup.ssrcs)) { + ssrcGroup.ssrcs.forEach(function (ssrc) { + if (typeof ssrc2group[ssrc] === 'undefined') { + ssrc2group[ssrc] = []; + } + + ssrc2group[ssrc].push(ssrcGroup); + }); + } + }); + } + + // ssrc to m-line index. + var ssrc2ml = {}; + + if (typeof sources === 'object') { + + // We'll use the "bLine" object as a prototype for each new "mLine" + // that we create, but first we need to clean it up a bit. + delete bLine.sources; + delete bLine.ssrcGroups; + delete bLine.candidates; + delete bLine.iceUfrag; + delete bLine.icePwd; + delete bLine.fingerprint; + delete bLine.port; + delete bLine.mid; + + // Explode the Plan B channel sources with one m-line per source. + Object.keys(sources).forEach(function(ssrc) { + + // The (unified) m-line for this SSRC. We either create it from + // scratch or, if it's a grouped SSRC, we re-use a related + // mline. In other words, if the source is grouped with another + // source, put the two together in the same m-line. + var uLine; + + // We assume here that we are the answerer in the O/A, so any + // offers which we translate come from the remote side, while + // answers are local. So the check below is to make that we + // handle receive-only SSRCs in a special way only if they come + // from the remote side. + if (desc.type==='offer') { + // We want to detect SSRCs which are used by a remote peer + // in an m-line with direction=recvonly (i.e. they are + // being used for RTCP only). + // This information would have gotten lost if the remote + // peer used Unified Plan and their local description was + // translated to Plan B. So we use the lack of an MSID + // attribute to deduce a "receive only" SSRC. + if (!sources[ssrc].msid) { + recvonlySsrcs[bLine.type][ssrc] = sources[ssrc]; + // Receive-only SSRCs must not create new m-lines. We + // will assign them to an existing m-line later. + return; + } + } + + if (typeof ssrc2group[ssrc] !== 'undefined' && + Array.isArray(ssrc2group[ssrc])) { + ssrc2group[ssrc].some(function (ssrcGroup) { + // ssrcGroup.ssrcs *is* an Array, no need to check + // again here. + return ssrcGroup.ssrcs.some(function (related) { + if (typeof ssrc2ml[related] === 'object') { + uLine = ssrc2ml[related]; + return true; + } + }); + }); + } + + if (typeof uLine === 'object') { + // the m-line already exists. Just add the source. + uLine.sources[ssrc] = sources[ssrc]; + delete sources[ssrc].msid; + } else { + // Use the "bLine" as a prototype for the "uLine". + uLine = Object.create(bLine); + ssrc2ml[ssrc] = uLine; + + if (typeof sources[ssrc].msid !== 'undefined') { + // Assign the msid of the source to the m-line. Note + // that it is not guaranteed that the source will have + // msid. In particular "recvonly" sources don't have an + // msid. Note that "recvonly" is a term only defined + // for m-lines. + uLine.msid = sources[ssrc].msid; + delete sources[ssrc].msid; + } + + // We assign one SSRC per media line. + uLine.sources = {}; + uLine.sources[ssrc] = sources[ssrc]; + uLine.ssrcGroups = ssrc2group[ssrc]; + + // Use the cached Unified Plan SDP (if it exists) to assign + // SSRCs to mids. + if (typeof cached !== 'undefined' && + typeof cached.media !== 'undefined' && + Array.isArray(cached.media)) { + + cached.media.forEach(function (m) { + if (typeof m.sources === 'object') { + Object.keys(m.sources).forEach(function (s) { + if (s === ssrc) { + uLine.mid = m.mid; + } + }); + } + }); + } + + if (typeof uLine.mid === 'undefined') { + + // If this is an SSRC that we see for the first time + // assign it a new mid. This is typically the case when + // this method is called to transform a remote + // description for the first time or when there is a + // new SSRC in the remote description because a new + // peer has joined the conference. Local SSRCs should + // have already been added to the map in the toPlanB + // method. + // + // Because FF generates answers in Unified Plan style, + // we MUST already have a cached answer with all the + // local SSRCs mapped to some m-line/mid. + + uLine.mid = [bLine.type, '-', ssrc].join(''); + } + + // Include the candidates in the 1st media line. + uLine.candidates = candidates; + uLine.iceUfrag = iceUfrag; + uLine.icePwd = icePwd; + uLine.fingerprint = fingerprint; + uLine.port = port; + + mid2ul[uLine.mid] = uLine; + sources2ul[uIdx] = uLine.sources; + + self.cache.mlU2BMap[uIdx] = bIdx; + if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') { + self.cache.mlB2UMap[bIdx] = uIdx; + } + uIdx++; + } + }); + } else { + var uLine = bLine; + + uLine.candidates = candidates; + uLine.iceUfrag = iceUfrag; + uLine.icePwd = icePwd; + uLine.fingerprint = fingerprint; + uLine.port = port; + + mid2ul[uLine.mid] = uLine; + + self.cache.mlU2BMap[uIdx] = bIdx; + if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') { + self.cache.mlB2UMap[bIdx] = uIdx; + } + } + + bIdx++; + }); + + // Rebuild the media array in the right order and add the missing mLines + // (missing from the Plan B SDP). + session.media = []; + mids = []; // reuse + + if (desc.type === 'answer') { + + // The media lines in the answer must match the media lines in the + // offer. The order is important too. Here we assume that Firefox is + // the answerer, so we merely have to use the reconstructed (unified) + // answer to update the cached (unified) answer accordingly. + // + // In the general case, one would have to use the cached (unified) + // offer to find the m-lines that are missing from the reconstructed + // answer, potentially grabbing them from the cached (unified) answer. + // One has to be careful with this approach because inactive m-lines do + // not always have an mid, making it tricky (impossible?) to find where + // exactly and which m-lines are missing from the reconstructed answer. + + for (var i = 0; i < cached.media.length; i++) { + var uLine = cached.media[i]; + + delete uLine.msid; + delete uLine.sources; + delete uLine.ssrcGroups; + + if (typeof sources2ul[i] === 'undefined') { + if (!uLine.direction + || uLine.direction === 'sendrecv') + uLine.direction = 'recvonly'; + else if (uLine.direction === 'sendonly') + uLine.direction = 'inactive'; + } else { + if (!uLine.direction + || uLine.direction === 'sendrecv') + uLine.direction = 'sendrecv'; + else if (uLine.direction === 'recvonly') + uLine.direction = 'sendonly'; + } + + uLine.sources = sources2ul[i]; + uLine.candidates = candidates; + uLine.iceUfrag = iceUfrag; + uLine.icePwd = icePwd; + uLine.fingerprint = fingerprint; + + uLine.rtp = rtp[uLine.type]; + uLine.payloads = payloads[uLine.type]; + uLine.rtcpFb = rtcpFb[uLine.type]; + + session.media.push(uLine); + + if (typeof uLine.mid === 'string') { + // inactive lines don't/may not have an mid. + mids.push(uLine.mid); + } + } + } else { + + // SDP offer/answer (and the JSEP spec) forbids removing an m-section + // under any circumstances. If we are no longer interested in sending a + // track, we just remove the msid and ssrc attributes and set it to + // either a=recvonly (as the reofferer, we must use recvonly if the + // other side was previously sending on the m-section, but we can also + // leave the possibility open if it wasn't previously in use), or + // a=inactive. + + if (typeof cached !== 'undefined' && + typeof cached.media !== 'undefined' && + Array.isArray(cached.media)) { + cached.media.forEach(function(uLine) { + mids.push(uLine.mid); + if (typeof mid2ul[uLine.mid] !== 'undefined') { + session.media.push(mid2ul[uLine.mid]); + } else { + delete uLine.msid; + delete uLine.sources; + delete uLine.ssrcGroups; + + if (!uLine.direction + || uLine.direction === 'sendrecv') { + uLine.direction = 'sendonly'; + } + if (!uLine.direction + || uLine.direction === 'recvonly') { + uLine.direction = 'inactive'; + } + + addSetupAttr (uLine); + session.media.push(uLine); + } + }); + } + + // Add all the remaining (new) m-lines of the transformed SDP. + Object.keys(mid2ul).forEach(function(mid) { + if (mids.indexOf(mid) === -1) { + mids.push(mid); + if (mid2ul[mid].direction === 'recvonly') { + // This is a remote recvonly channel. Add its SSRC to the + // appropriate sendrecv or sendonly channel. + // TODO(gp) what if we don't have sendrecv/sendonly + // channel? + + var done = false; + + session.media.some(function (uLine) { + if ((uLine.direction === 'sendrecv' || + uLine.direction === 'sendonly') && + uLine.type === mid2ul[mid].type) { + // mid2ul[mid] shouldn't have any ssrc-groups + Object.keys(mid2ul[mid].sources).forEach( + function (ssrc) { + uLine.sources[ssrc] = + mid2ul[mid].sources[ssrc]; + }); + + done = true; + return true; + } + }); + + if (!done) { + session.media.push(mid2ul[mid]); + } + } else { + session.media.push(mid2ul[mid]); + } + } + }); + } + + // After we have constructed the Plan Unified m-lines we can figure out + // where (in which m-line) to place the 'recvonly SSRCs'. + // Note: we assume here that we are the answerer in the O/A, so any offers + // which we translate come from the remote side, while answers are local + // (and so our last local description is cached as an 'answer'). + ["audio", "video"].forEach(function (type) { + if (!session || !session.media || !Array.isArray(session.media)) + return; + + var idx = null; + if (Object.keys(recvonlySsrcs[type]).length > 0) { + idx = self.getFirstSendingIndexFromAnswer(type); + if (idx === null){ + // If this is the first offer we receive, we don't have a + // cached answer. Assume that we will be sending media using + // the first m-line for each media type. + + for (var i = 0; i < session.media.length; i++) { + if (session.media[i].type === type) { + idx = i; + break; + } + } + } + } + + if (idx && session.media.length > idx) { + var mLine = session.media[idx]; + Object.keys(recvonlySsrcs[type]).forEach(function(ssrc) { + if (mLine.sources && mLine.sources[ssrc]) { + console.warn("Replacing an existing SSRC."); + } + if (!mLine.sources) { + mLine.sources = {}; + } + + mLine.sources[ssrc] = recvonlySsrcs[type][ssrc]; + }); + } + }); + + if (typeof session.groups !== 'undefined') { + // We regenerate the BUNDLE group (since we regenerated the mids) + session.groups.some(function(group) { + if (group.type === 'BUNDLE') { + group.mids = mids.join(' '); + return true; + } + }); + } + + // msid semantic + session.msidSemantic = { + semantic: 'WMS', + token: '*' + }; + + var resStr = transform.write(session); + + // Cache the transformed SDP (Unified Plan) for later re-use in this + // function. + this.cache[desc.type] = resStr; + + return new RTCSessionDescription({ + type: desc.type, + sdp: resStr + }); + + //#endregion +}; + +},{"./array-equals":29,"./transform":32}],32:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var transform = require('sdp-transform'); + +exports.write = function(session, opts) { + + if (typeof session !== 'undefined' && + typeof session.media !== 'undefined' && + Array.isArray(session.media)) { + + session.media.forEach(function (mLine) { + // expand sources to ssrcs + if (typeof mLine.sources !== 'undefined' && + Object.keys(mLine.sources).length !== 0) { + mLine.ssrcs = []; + Object.keys(mLine.sources).forEach(function (ssrc) { + var source = mLine.sources[ssrc]; + Object.keys(source).forEach(function (attribute) { + mLine.ssrcs.push({ + id: ssrc, + attribute: attribute, + value: source[attribute] + }); + }); + }); + delete mLine.sources; + } + + // join ssrcs in ssrc groups + if (typeof mLine.ssrcGroups !== 'undefined' && + Array.isArray(mLine.ssrcGroups)) { + mLine.ssrcGroups.forEach(function (ssrcGroup) { + if (typeof ssrcGroup.ssrcs !== 'undefined' && + Array.isArray(ssrcGroup.ssrcs)) { + ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' '); + } + }); + } + }); + } + + // join group mids + if (typeof session !== 'undefined' && + typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { + + session.groups.forEach(function (g) { + if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) { + g.mids = g.mids.join(' '); + } + }); + } + + return transform.write(session, opts); +}; + +exports.parse = function(sdp) { + var session = transform.parse(sdp); + + if (typeof session !== 'undefined' && typeof session.media !== 'undefined' && + Array.isArray(session.media)) { + + session.media.forEach(function (mLine) { + // group sources attributes by ssrc + if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) { + mLine.sources = {}; + mLine.ssrcs.forEach(function (ssrc) { + if (!mLine.sources[ssrc.id]) + mLine.sources[ssrc.id] = {}; + mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value; + }); + + delete mLine.ssrcs; + } + + // split ssrcs in ssrc groups + if (typeof mLine.ssrcGroups !== 'undefined' && + Array.isArray(mLine.ssrcGroups)) { + mLine.ssrcGroups.forEach(function (ssrcGroup) { + if (typeof ssrcGroup.ssrcs === 'string') { + ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' '); + } + }); + } + }); + } + // split group mids + if (typeof session !== 'undefined' && + typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { + + session.groups.forEach(function (g) { + if (typeof g.mids === 'string') { + g.mids = g.mids.split(' '); + } + }); + } + + return session; +}; + + +},{"sdp-transform":26}],33:[function(require,module,exports){ + /* eslint-env node */ +'use strict'; + +// SDP helpers. +var SDPUtils = {}; + +// Generate an alphanumeric identifier for cname or mids. +// TODO: use UUIDs instead? https://gist.github.com/jed/982883 +SDPUtils.generateIdentifier = function() { + return Math.random().toString(36).substr(2, 10); +}; + +// The RTCP CNAME used by all peerconnections from the same JS. +SDPUtils.localCName = SDPUtils.generateIdentifier(); + +// Splits SDP into lines, dealing with both CRLF and LF. +SDPUtils.splitLines = function(blob) { + return blob.trim().split('\n').map(function(line) { + return line.trim(); + }); +}; +// Splits SDP into sessionpart and mediasections. Ensures CRLF. +SDPUtils.splitSections = function(blob) { + var parts = blob.split('\nm='); + return parts.map(function(part, index) { + return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; + }); +}; + +// Returns lines that start with a certain prefix. +SDPUtils.matchPrefix = function(blob, prefix) { + return SDPUtils.splitLines(blob).filter(function(line) { + return line.indexOf(prefix) === 0; + }); +}; + +// Parses an ICE candidate line. Sample input: +// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 +// rport 55996" +SDPUtils.parseCandidate = function(line) { + var parts; + // Parse both variants. + if (line.indexOf('a=candidate:') === 0) { + parts = line.substring(12).split(' '); + } else { + parts = line.substring(10).split(' '); + } + + var candidate = { + foundation: parts[0], + component: parts[1], + protocol: parts[2].toLowerCase(), + priority: parseInt(parts[3], 10), + ip: parts[4], + port: parseInt(parts[5], 10), + // skip parts[6] == 'typ' + type: parts[7] + }; + + for (var i = 8; i < parts.length; i += 2) { + switch (parts[i]) { + case 'raddr': + candidate.relatedAddress = parts[i + 1]; + break; + case 'rport': + candidate.relatedPort = parseInt(parts[i + 1], 10); + break; + case 'tcptype': + candidate.tcpType = parts[i + 1]; + break; + default: // Unknown extensions are silently ignored. + break; + } + } + return candidate; +}; + +// Translates a candidate object into SDP candidate attribute. +SDPUtils.writeCandidate = function(candidate) { + var sdp = []; + sdp.push(candidate.foundation); + sdp.push(candidate.component); + sdp.push(candidate.protocol.toUpperCase()); + sdp.push(candidate.priority); + sdp.push(candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push('typ'); + sdp.push(type); + if (type !== 'host' && candidate.relatedAddress && + candidate.relatedPort) { + sdp.push('raddr'); + sdp.push(candidate.relatedAddress); // was: relAddr + sdp.push('rport'); + sdp.push(candidate.relatedPort); // was: relPort + } + if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { + sdp.push('tcptype'); + sdp.push(candidate.tcpType); + } + return 'candidate:' + sdp.join(' '); +}; + +// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: +// a=rtpmap:111 opus/48000/2 +SDPUtils.parseRtpMap = function(line) { + var parts = line.substr(9).split(' '); + var parsed = { + payloadType: parseInt(parts.shift(), 10) // was: id + }; + + parts = parts[0].split('/'); + + parsed.name = parts[0]; + parsed.clockRate = parseInt(parts[1], 10); // was: clockrate + // was: channels + parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; + return parsed; +}; + +// Generate an a=rtpmap line from RTCRtpCodecCapability or +// RTCRtpCodecParameters. +SDPUtils.writeRtpMap = function(codec) { + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + + (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; +}; + +// Parses an a=extmap line (headerextension from RFC 5285). Sample input: +// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset +SDPUtils.parseExtmap = function(line) { + var parts = line.substr(9).split(' '); + return { + id: parseInt(parts[0], 10), + uri: parts[1] + }; +}; + +// Generates a=extmap line from RTCRtpHeaderExtensionParameters or +// RTCRtpHeaderExtension. +SDPUtils.writeExtmap = function(headerExtension) { + return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + + ' ' + headerExtension.uri + '\r\n'; +}; + +// Parses an ftmp line, returns dictionary. Sample input: +// a=fmtp:96 vbr=on;cng=on +// Also deals with vbr=on; cng=on +SDPUtils.parseFmtp = function(line) { + var parsed = {}; + var kv; + var parts = line.substr(line.indexOf(' ') + 1).split(';'); + for (var j = 0; j < parts.length; j++) { + kv = parts[j].trim().split('='); + parsed[kv[0].trim()] = kv[1]; + } + return parsed; +}; + +// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeFmtp = function(codec) { + var line = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.parameters && Object.keys(codec.parameters).length) { + var params = []; + Object.keys(codec.parameters).forEach(function(param) { + params.push(param + '=' + codec.parameters[param]); + }); + line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; + } + return line; +}; + +// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: +// a=rtcp-fb:98 nack rpsi +SDPUtils.parseRtcpFb = function(line) { + var parts = line.substr(line.indexOf(' ') + 1).split(' '); + return { + type: parts.shift(), + parameter: parts.join(' ') + }; +}; +// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeRtcpFb = function(codec) { + var lines = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.rtcpFeedback && codec.rtcpFeedback.length) { + // FIXME: special handling for trr-int? + codec.rtcpFeedback.forEach(function(fb) { + lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + + '\r\n'; + }); + } + return lines; +}; + +// Parses an RFC 5576 ssrc media attribute. Sample input: +// a=ssrc:3735928559 cname:something +SDPUtils.parseSsrcMedia = function(line) { + var sp = line.indexOf(' '); + var parts = { + ssrc: parseInt(line.substr(7, sp - 7), 10) + }; + var colon = line.indexOf(':', sp); + if (colon > -1) { + parts.attribute = line.substr(sp + 1, colon - sp - 1); + parts.value = line.substr(colon + 1); + } else { + parts.attribute = line.substr(sp + 1); + } + return parts; +}; + +// Extracts DTLS parameters from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the fingerprint line as input. See also getIceParameters. +SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.splitLines(mediaSection); + // Search in session part, too. + lines = lines.concat(SDPUtils.splitLines(sessionpart)); + var fpLine = lines.filter(function(line) { + return line.indexOf('a=fingerprint:') === 0; + })[0].substr(14); + // Note: a=setup line is ignored since we use the 'auto' role. + var dtlsParameters = { + role: 'auto', + fingerprints: [{ + algorithm: fpLine.split(' ')[0], + value: fpLine.split(' ')[1] + }] + }; + return dtlsParameters; +}; + +// Serializes DTLS parameters to SDP. +SDPUtils.writeDtlsParameters = function(params, setupType) { + var sdp = 'a=setup:' + setupType + '\r\n'; + params.fingerprints.forEach(function(fp) { + sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; + }); + return sdp; +}; +// Parses ICE information from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the ice-ufrag and ice-pwd lines as input. +SDPUtils.getIceParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.splitLines(mediaSection); + // Search in session part, too. + lines = lines.concat(SDPUtils.splitLines(sessionpart)); + var iceParameters = { + usernameFragment: lines.filter(function(line) { + return line.indexOf('a=ice-ufrag:') === 0; + })[0].substr(12), + password: lines.filter(function(line) { + return line.indexOf('a=ice-pwd:') === 0; + })[0].substr(10) + }; + return iceParameters; +}; + +// Serializes ICE parameters to SDP. +SDPUtils.writeIceParameters = function(params) { + return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + + 'a=ice-pwd:' + params.password + '\r\n'; +}; + +// Parses the SDP media section and returns RTCRtpParameters. +SDPUtils.parseRtpParameters = function(mediaSection) { + var description = { + codecs: [], + headerExtensions: [], + fecMechanisms: [], + rtcp: [] + }; + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] + var pt = mline[i]; + var rtpmapline = SDPUtils.matchPrefix( + mediaSection, 'a=rtpmap:' + pt + ' ')[0]; + if (rtpmapline) { + var codec = SDPUtils.parseRtpMap(rtpmapline); + var fmtps = SDPUtils.matchPrefix( + mediaSection, 'a=fmtp:' + pt + ' '); + // Only the first a=fmtp: is considered. + codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; + codec.rtcpFeedback = SDPUtils.matchPrefix( + mediaSection, 'a=rtcp-fb:' + pt + ' ') + .map(SDPUtils.parseRtcpFb); + description.codecs.push(codec); + // parse FEC mechanisms from rtpmap lines. + switch (codec.name.toUpperCase()) { + case 'RED': + case 'ULPFEC': + description.fecMechanisms.push(codec.name.toUpperCase()); + break; + default: // only RED and ULPFEC are recognized as FEC mechanisms. + break; + } + } + } + SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) { + description.headerExtensions.push(SDPUtils.parseExtmap(line)); + }); + // FIXME: parse rtcp. + return description; +}; + +// Generates parts of the SDP media section describing the capabilities / +// parameters. +SDPUtils.writeRtpDescription = function(kind, caps) { + var sdp = ''; + + // Build the mline. + sdp += 'm=' + kind + ' '; + sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. + sdp += ' UDP/TLS/RTP/SAVPF '; + sdp += caps.codecs.map(function(codec) { + if (codec.preferredPayloadType !== undefined) { + return codec.preferredPayloadType; + } + return codec.payloadType; + }).join(' ') + '\r\n'; + + sdp += 'c=IN IP4 0.0.0.0\r\n'; + sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; + + // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. + caps.codecs.forEach(function(codec) { + sdp += SDPUtils.writeRtpMap(codec); + sdp += SDPUtils.writeFmtp(codec); + sdp += SDPUtils.writeRtcpFb(codec); + }); + // FIXME: add headerExtensions, fecMechanismÅŸ and rtcp. + sdp += 'a=rtcp-mux\r\n'; + return sdp; +}; + +// Parses the SDP media section and returns an array of +// RTCRtpEncodingParameters. +SDPUtils.parseRtpEncodingParameters = function(mediaSection) { + var encodingParameters = []; + var description = SDPUtils.parseRtpParameters(mediaSection); + var hasRed = description.fecMechanisms.indexOf('RED') !== -1; + var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; + + // filter a=ssrc:... cname:, ignore PlanB-msid + var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(parts) { + return parts.attribute === 'cname'; + }); + var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; + var secondarySsrc; + + var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') + .map(function(line) { + var parts = line.split(' '); + parts.shift(); + return parts.map(function(part) { + return parseInt(part, 10); + }); + }); + if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { + secondarySsrc = flows[0][1]; + } + + description.codecs.forEach(function(codec) { + if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { + var encParam = { + ssrc: primarySsrc, + codecPayloadType: parseInt(codec.parameters.apt, 10), + rtx: { + payloadType: codec.payloadType, + ssrc: secondarySsrc + } + }; + encodingParameters.push(encParam); + if (hasRed) { + encParam = JSON.parse(JSON.stringify(encParam)); + encParam.fec = { + ssrc: secondarySsrc, + mechanism: hasUlpfec ? 'red+ulpfec' : 'red' + }; + encodingParameters.push(encParam); + } + } + }); + if (encodingParameters.length === 0 && primarySsrc) { + encodingParameters.push({ + ssrc: primarySsrc + }); + } + + // we support both b=AS and b=TIAS but interpret AS as TIAS. + var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); + if (bandwidth.length) { + if (bandwidth[0].indexOf('b=TIAS:') === 0) { + bandwidth = parseInt(bandwidth[0].substr(7), 10); + } else if (bandwidth[0].indexOf('b=AS:') === 0) { + bandwidth = parseInt(bandwidth[0].substr(5), 10); + } + encodingParameters.forEach(function(params) { + params.maxBitrate = bandwidth; + }); + } + return encodingParameters; +}; + +SDPUtils.writeSessionBoilerplate = function() { + // FIXME: sess-id should be an NTP timestamp. + return 'v=0\r\n' + + 'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' + + 's=-\r\n' + + 't=0 0\r\n'; +}; + +SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { + var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); + + // Map ICE parameters (ufrag, pwd) to SDP. + sdp += SDPUtils.writeIceParameters( + transceiver.iceGatherer.getLocalParameters()); + + // Map DTLS parameters to SDP. + sdp += SDPUtils.writeDtlsParameters( + transceiver.dtlsTransport.getLocalParameters(), + type === 'offer' ? 'actpass' : 'active'); + + sdp += 'a=mid:' + transceiver.mid + '\r\n'; + + if (transceiver.rtpSender && transceiver.rtpReceiver) { + sdp += 'a=sendrecv\r\n'; + } else if (transceiver.rtpSender) { + sdp += 'a=sendonly\r\n'; + } else if (transceiver.rtpReceiver) { + sdp += 'a=recvonly\r\n'; + } else { + sdp += 'a=inactive\r\n'; + } + + // FIXME: for RTX there might be multiple SSRCs. Not implemented in Edge yet. + if (transceiver.rtpSender) { + var msid = 'msid:' + stream.id + ' ' + + transceiver.rtpSender.track.id + '\r\n'; + sdp += 'a=' + msid; + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' ' + msid; + } + // FIXME: this should be written by writeRtpDescription. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + return sdp; +}; + +// Gets the direction from the mediaSection or the sessionpart. +SDPUtils.getDirection = function(mediaSection, sessionpart) { + // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. + var lines = SDPUtils.splitLines(mediaSection); + for (var i = 0; i < lines.length; i++) { + switch (lines[i]) { + case 'a=sendrecv': + case 'a=sendonly': + case 'a=recvonly': + case 'a=inactive': + return lines[i].substr(2); + default: + // FIXME: What should happen here? + } + } + if (sessionpart) { + return SDPUtils.getDirection(sessionpart); + } + return 'sendrecv'; +}; + +// Expose public methods. +module.exports = SDPUtils; + +},{}],34:[function(require,module,exports){ +(function (global){ +'use strict'; + +var transportList = require('./transport-list'); + +module.exports = require('./main')(transportList); + +// TODO can't get rid of this until all servers do +if ('_sockjs_onload' in global) { + setTimeout(global._sockjs_onload, 1); +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./main":47,"./transport-list":49}],35:[function(require,module,exports){ +'use strict'; + +var inherits = require('inherits') + , Event = require('./event') + ; + +function CloseEvent() { + Event.call(this); + this.initEvent('close', false, false); + this.wasClean = false; + this.code = 0; + this.reason = ''; +} + +inherits(CloseEvent, Event); + +module.exports = CloseEvent; + +},{"./event":37,"inherits":7}],36:[function(require,module,exports){ +'use strict'; + +var inherits = require('inherits') + , EventTarget = require('./eventtarget') + ; + +function EventEmitter() { + EventTarget.call(this); +} + +inherits(EventEmitter, EventTarget); + +EventEmitter.prototype.removeAllListeners = function(type) { + if (type) { + delete this._listeners[type]; + } else { + this._listeners = {}; + } +}; + +EventEmitter.prototype.once = function(type, listener) { + var self = this + , fired = false; + + function g() { + self.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + this.on(type, g); +}; + +EventEmitter.prototype.emit = function() { + var type = arguments[0]; + var listeners = this._listeners[type]; + if (!listeners) { + return; + } + // equivalent of Array.prototype.slice.call(arguments, 1); + var l = arguments.length; + var args = new Array(l - 1); + for (var ai = 1; ai < l; ai++) { + args[ai - 1] = arguments[ai]; + } + for (var i = 0; i < listeners.length; i++) { + listeners[i].apply(this, args); + } +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener = EventTarget.prototype.addEventListener; +EventEmitter.prototype.removeListener = EventTarget.prototype.removeEventListener; + +module.exports.EventEmitter = EventEmitter; + +},{"./eventtarget":38,"inherits":7}],37:[function(require,module,exports){ +'use strict'; + +function Event(eventType) { + this.type = eventType; +} + +Event.prototype.initEvent = function(eventType, canBubble, cancelable) { + this.type = eventType; + this.bubbles = canBubble; + this.cancelable = cancelable; + this.timeStamp = +new Date(); + return this; +}; + +Event.prototype.stopPropagation = function() {}; +Event.prototype.preventDefault = function() {}; + +Event.CAPTURING_PHASE = 1; +Event.AT_TARGET = 2; +Event.BUBBLING_PHASE = 3; + +module.exports = Event; + +},{}],38:[function(require,module,exports){ +'use strict'; + +/* Simplified implementation of DOM2 EventTarget. + * http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget + */ + +function EventTarget() { + this._listeners = {}; +} + +EventTarget.prototype.addEventListener = function(eventType, listener) { + if (!(eventType in this._listeners)) { + this._listeners[eventType] = []; + } + var arr = this._listeners[eventType]; + // #4 + if (arr.indexOf(listener) === -1) { + // Make a copy so as not to interfere with a current dispatchEvent. + arr = arr.concat([listener]); + } + this._listeners[eventType] = arr; +}; + +EventTarget.prototype.removeEventListener = function(eventType, listener) { + var arr = this._listeners[eventType]; + if (!arr) { + return; + } + var idx = arr.indexOf(listener); + if (idx !== -1) { + if (arr.length > 1) { + // Make a copy so as not to interfere with a current dispatchEvent. + this._listeners[eventType] = arr.slice(0, idx).concat(arr.slice(idx + 1)); + } else { + delete this._listeners[eventType]; + } + return; + } +}; + +EventTarget.prototype.dispatchEvent = function() { + var event = arguments[0]; + var t = event.type; + // equivalent of Array.prototype.slice.call(arguments, 0); + var args = arguments.length === 1 ? [event] : Array.apply(null, arguments); + // TODO: This doesn't match the real behavior; per spec, onfoo get + // their place in line from the /first/ time they're set from + // non-null. Although WebKit bumps it to the end every time it's + // set. + if (this['on' + t]) { + this['on' + t].apply(this, args); + } + if (t in this._listeners) { + // Grab a reference to the listeners list. removeEventListener may alter the list. + var listeners = this._listeners[t]; + for (var i = 0; i < listeners.length; i++) { + listeners[i].apply(this, args); + } + } +}; + +module.exports = EventTarget; + +},{}],39:[function(require,module,exports){ +'use strict'; + +var inherits = require('inherits') + , Event = require('./event') + ; + +function TransportMessageEvent(data) { + Event.call(this); + this.initEvent('message', false, false); + this.data = data; +} + +inherits(TransportMessageEvent, Event); + +module.exports = TransportMessageEvent; + +},{"./event":37,"inherits":7}],40:[function(require,module,exports){ +'use strict'; + +var JSON3 = require('json3') + , iframeUtils = require('./utils/iframe') + ; + +function FacadeJS(transport) { + this._transport = transport; + transport.on('message', this._transportMessage.bind(this)); + transport.on('close', this._transportClose.bind(this)); +} + +FacadeJS.prototype._transportClose = function(code, reason) { + iframeUtils.postMessage('c', JSON3.stringify([code, reason])); +}; +FacadeJS.prototype._transportMessage = function(frame) { + iframeUtils.postMessage('t', frame); +}; +FacadeJS.prototype._send = function(data) { + this._transport.send(data); +}; +FacadeJS.prototype._close = function() { + this._transport.close(); + this._transport.removeAllListeners(); +}; + +module.exports = FacadeJS; + +},{"./utils/iframe":80,"json3":8}],41:[function(require,module,exports){ +(function (process){ +'use strict'; + +var urlUtils = require('./utils/url') + , eventUtils = require('./utils/event') + , JSON3 = require('json3') + , FacadeJS = require('./facade') + , InfoIframeReceiver = require('./info-iframe-receiver') + , iframeUtils = require('./utils/iframe') + , loc = require('./location') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:iframe-bootstrap'); +} + +module.exports = function(SockJS, availableTransports) { + var transportMap = {}; + availableTransports.forEach(function(at) { + if (at.facadeTransport) { + transportMap[at.facadeTransport.transportName] = at.facadeTransport; + } + }); + + // hard-coded for the info iframe + // TODO see if we can make this more dynamic + transportMap[InfoIframeReceiver.transportName] = InfoIframeReceiver; + var parentOrigin; + + /* eslint-disable camelcase */ + SockJS.bootstrap_iframe = function() { + /* eslint-enable camelcase */ + var facade; + iframeUtils.currentWindowId = loc.hash.slice(1); + var onMessage = function(e) { + if (e.source !== parent) { + return; + } + if (typeof parentOrigin === 'undefined') { + parentOrigin = e.origin; + } + if (e.origin !== parentOrigin) { + return; + } + + var iframeMessage; + try { + iframeMessage = JSON3.parse(e.data); + } catch (ignored) { + debug('bad json', e.data); + return; + } + + if (iframeMessage.windowId !== iframeUtils.currentWindowId) { + return; + } + switch (iframeMessage.type) { + case 's': + var p; + try { + p = JSON3.parse(iframeMessage.data); + } catch (ignored) { + debug('bad json', iframeMessage.data); + break; + } + var version = p[0]; + var transport = p[1]; + var transUrl = p[2]; + var baseUrl = p[3]; + debug(version, transport, transUrl, baseUrl); + // change this to semver logic + if (version !== SockJS.version) { + throw new Error('Incompatible SockJS! Main site uses:' + + ' "' + version + '", the iframe:' + + ' "' + SockJS.version + '".'); + } + + if (!urlUtils.isOriginEqual(transUrl, loc.href) || + !urlUtils.isOriginEqual(baseUrl, loc.href)) { + throw new Error('Can\'t connect to different domain from within an ' + + 'iframe. (' + loc.href + ', ' + transUrl + ', ' + baseUrl + ')'); + } + facade = new FacadeJS(new transportMap[transport](transUrl, baseUrl)); + break; + case 'm': + facade._send(iframeMessage.data); + break; + case 'c': + if (facade) { + facade._close(); + } + facade = null; + break; + } + }; + + eventUtils.attachEvent('message', onMessage); + + // Start + iframeUtils.postMessage('s'); + }; +}; + +}).call(this,require('_process')) + +},{"./facade":40,"./info-iframe-receiver":43,"./location":46,"./utils/event":79,"./utils/iframe":80,"./utils/url":85,"_process":110,"debug":1,"json3":8}],42:[function(require,module,exports){ +(function (process){ +'use strict'; + +var EventEmitter = require('events').EventEmitter + , inherits = require('inherits') + , JSON3 = require('json3') + , objectUtils = require('./utils/object') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:info-ajax'); +} + +function InfoAjax(url, AjaxObject) { + EventEmitter.call(this); + + var self = this; + var t0 = +new Date(); + this.xo = new AjaxObject('GET', url); + + this.xo.once('finish', function(status, text) { + var info, rtt; + if (status === 200) { + rtt = (+new Date()) - t0; + if (text) { + try { + info = JSON3.parse(text); + } catch (e) { + debug('bad json', text); + } + } + + if (!objectUtils.isObject(info)) { + info = {}; + } + } + self.emit('finish', info, rtt); + self.removeAllListeners(); + }); +} + +inherits(InfoAjax, EventEmitter); + +InfoAjax.prototype.close = function() { + this.removeAllListeners(); + this.xo.close(); +}; + +module.exports = InfoAjax; + +}).call(this,require('_process')) + +},{"./utils/object":82,"_process":110,"debug":1,"events":36,"inherits":7,"json3":8}],43:[function(require,module,exports){ +'use strict'; + +var inherits = require('inherits') + , EventEmitter = require('events').EventEmitter + , JSON3 = require('json3') + , XHRLocalObject = require('./transport/sender/xhr-local') + , InfoAjax = require('./info-ajax') + ; + +function InfoReceiverIframe(transUrl) { + var self = this; + EventEmitter.call(this); + + this.ir = new InfoAjax(transUrl, XHRLocalObject); + this.ir.once('finish', function(info, rtt) { + self.ir = null; + self.emit('message', JSON3.stringify([info, rtt])); + }); +} + +inherits(InfoReceiverIframe, EventEmitter); + +InfoReceiverIframe.transportName = 'iframe-info-receiver'; + +InfoReceiverIframe.prototype.close = function() { + if (this.ir) { + this.ir.close(); + this.ir = null; + } + this.removeAllListeners(); +}; + +module.exports = InfoReceiverIframe; + +},{"./info-ajax":42,"./transport/sender/xhr-local":70,"events":36,"inherits":7,"json3":8}],44:[function(require,module,exports){ +(function (process,global){ +'use strict'; + +var EventEmitter = require('events').EventEmitter + , inherits = require('inherits') + , JSON3 = require('json3') + , utils = require('./utils/event') + , IframeTransport = require('./transport/iframe') + , InfoReceiverIframe = require('./info-iframe-receiver') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:info-iframe'); +} + +function InfoIframe(baseUrl, url) { + var self = this; + EventEmitter.call(this); + + var go = function() { + var ifr = self.ifr = new IframeTransport(InfoReceiverIframe.transportName, url, baseUrl); + + ifr.once('message', function(msg) { + if (msg) { + var d; + try { + d = JSON3.parse(msg); + } catch (e) { + debug('bad json', msg); + self.emit('finish'); + self.close(); + return; + } + + var info = d[0], rtt = d[1]; + self.emit('finish', info, rtt); + } + self.close(); + }); + + ifr.once('close', function() { + self.emit('finish'); + self.close(); + }); + }; + + // TODO this seems the same as the 'needBody' from transports + if (!global.document.body) { + utils.attachEvent('load', go); + } else { + go(); + } +} + +inherits(InfoIframe, EventEmitter); + +InfoIframe.enabled = function() { + return IframeTransport.enabled(); +}; + +InfoIframe.prototype.close = function() { + if (this.ifr) { + this.ifr.close(); + } + this.removeAllListeners(); + this.ifr = null; +}; + +module.exports = InfoIframe; + +}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./info-iframe-receiver":43,"./transport/iframe":55,"./utils/event":79,"_process":110,"debug":1,"events":36,"inherits":7,"json3":8}],45:[function(require,module,exports){ +(function (process){ +'use strict'; + +var EventEmitter = require('events').EventEmitter + , inherits = require('inherits') + , urlUtils = require('./utils/url') + , XDR = require('./transport/sender/xdr') + , XHRCors = require('./transport/sender/xhr-cors') + , XHRLocal = require('./transport/sender/xhr-local') + , XHRFake = require('./transport/sender/xhr-fake') + , InfoIframe = require('./info-iframe') + , InfoAjax = require('./info-ajax') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:info-receiver'); +} + +function InfoReceiver(baseUrl, urlInfo) { + debug(baseUrl); + var self = this; + EventEmitter.call(this); + + setTimeout(function() { + self.doXhr(baseUrl, urlInfo); + }, 0); +} + +inherits(InfoReceiver, EventEmitter); + +// TODO this is currently ignoring the list of available transports and the whitelist + +InfoReceiver._getReceiver = function(baseUrl, url, urlInfo) { + // determine method of CORS support (if needed) + if (urlInfo.sameOrigin) { + return new InfoAjax(url, XHRLocal); + } + if (XHRCors.enabled) { + return new InfoAjax(url, XHRCors); + } + if (XDR.enabled && urlInfo.sameScheme) { + return new InfoAjax(url, XDR); + } + if (InfoIframe.enabled()) { + return new InfoIframe(baseUrl, url); + } + return new InfoAjax(url, XHRFake); +}; + +InfoReceiver.prototype.doXhr = function(baseUrl, urlInfo) { + var self = this + , url = urlUtils.addPath(baseUrl, '/info') + ; + debug('doXhr', url); + + this.xo = InfoReceiver._getReceiver(baseUrl, url, urlInfo); + + this.timeoutRef = setTimeout(function() { + debug('timeout'); + self._cleanup(false); + self.emit('finish'); + }, InfoReceiver.timeout); + + this.xo.once('finish', function(info, rtt) { + debug('finish', info, rtt); + self._cleanup(true); + self.emit('finish', info, rtt); + }); +}; + +InfoReceiver.prototype._cleanup = function(wasClean) { + debug('_cleanup'); + clearTimeout(this.timeoutRef); + this.timeoutRef = null; + if (!wasClean && this.xo) { + this.xo.close(); + } + this.xo = null; +}; + +InfoReceiver.prototype.close = function() { + debug('close'); + this.removeAllListeners(); + this._cleanup(false); +}; + +InfoReceiver.timeout = 8000; + +module.exports = InfoReceiver; + +}).call(this,require('_process')) + +},{"./info-ajax":42,"./info-iframe":44,"./transport/sender/xdr":67,"./transport/sender/xhr-cors":68,"./transport/sender/xhr-fake":69,"./transport/sender/xhr-local":70,"./utils/url":85,"_process":110,"debug":1,"events":36,"inherits":7}],46:[function(require,module,exports){ +(function (global){ +'use strict'; + +module.exports = global.location || { + origin: 'http://localhost:80' +, protocol: 'http' +, host: 'localhost' +, port: 80 +, href: 'http://localhost/' +, hash: '' +}; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{}],47:[function(require,module,exports){ +(function (process,global){ +'use strict'; + +require('./shims'); + +var URL = require('url-parse') + , inherits = require('inherits') + , JSON3 = require('json3') + , random = require('./utils/random') + , escape = require('./utils/escape') + , urlUtils = require('./utils/url') + , eventUtils = require('./utils/event') + , transport = require('./utils/transport') + , objectUtils = require('./utils/object') + , browser = require('./utils/browser') + , log = require('./utils/log') + , Event = require('./event/event') + , EventTarget = require('./event/eventtarget') + , loc = require('./location') + , CloseEvent = require('./event/close') + , TransportMessageEvent = require('./event/trans-message') + , InfoReceiver = require('./info-receiver') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:main'); +} + +var transports; + +// follow constructor steps defined at http://dev.w3.org/html5/websockets/#the-websocket-interface +function SockJS(url, protocols, options) { + if (!(this instanceof SockJS)) { + return new SockJS(url, protocols, options); + } + if (arguments.length < 1) { + throw new TypeError("Failed to construct 'SockJS: 1 argument required, but only 0 present"); + } + EventTarget.call(this); + + this.readyState = SockJS.CONNECTING; + this.extensions = ''; + this.protocol = ''; + + // non-standard extension + options = options || {}; + if (options.protocols_whitelist) { + log.warn("'protocols_whitelist' is DEPRECATED. Use 'transports' instead."); + } + this._transportsWhitelist = options.transports; + this._transportOptions = options.transportOptions || {}; + + var sessionId = options.sessionId || 8; + if (typeof sessionId === 'function') { + this._generateSessionId = sessionId; + } else if (typeof sessionId === 'number') { + this._generateSessionId = function() { + return random.string(sessionId); + }; + } else { + throw new TypeError('If sessionId is used in the options, it needs to be a number or a function.'); + } + + this._server = options.server || random.numberString(1000); + + // Step 1 of WS spec - parse and validate the url. Issue #8 + var parsedUrl = new URL(url); + if (!parsedUrl.host || !parsedUrl.protocol) { + throw new SyntaxError("The URL '" + url + "' is invalid"); + } else if (parsedUrl.hash) { + throw new SyntaxError('The URL must not contain a fragment'); + } else if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + throw new SyntaxError("The URL's scheme must be either 'http:' or 'https:'. '" + parsedUrl.protocol + "' is not allowed."); + } + + var secure = parsedUrl.protocol === 'https:'; + // Step 2 - don't allow secure origin with an insecure protocol + if (loc.protocol === 'https' && !secure) { + throw new Error('SecurityError: An insecure SockJS connection may not be initiated from a page loaded over HTTPS'); + } + + // Step 3 - check port access - no need here + // Step 4 - parse protocols argument + if (!protocols) { + protocols = []; + } else if (!Array.isArray(protocols)) { + protocols = [protocols]; + } + + // Step 5 - check protocols argument + var sortedProtocols = protocols.sort(); + sortedProtocols.forEach(function(proto, i) { + if (!proto) { + throw new SyntaxError("The protocols entry '" + proto + "' is invalid."); + } + if (i < (sortedProtocols.length - 1) && proto === sortedProtocols[i + 1]) { + throw new SyntaxError("The protocols entry '" + proto + "' is duplicated."); + } + }); + + // Step 6 - convert origin + var o = urlUtils.getOrigin(loc.href); + this._origin = o ? o.toLowerCase() : null; + + // remove the trailing slash + parsedUrl.set('pathname', parsedUrl.pathname.replace(/\/+$/, '')); + + // store the sanitized url + this.url = parsedUrl.href; + debug('using url', this.url); + + // Step 7 - start connection in background + // obtain server info + // http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-26 + this._urlInfo = { + nullOrigin: !browser.hasDomain() + , sameOrigin: urlUtils.isOriginEqual(this.url, loc.href) + , sameScheme: urlUtils.isSchemeEqual(this.url, loc.href) + }; + + this._ir = new InfoReceiver(this.url, this._urlInfo); + this._ir.once('finish', this._receiveInfo.bind(this)); +} + +inherits(SockJS, EventTarget); + +function userSetCode(code) { + return code === 1000 || (code >= 3000 && code <= 4999); +} + +SockJS.prototype.close = function(code, reason) { + // Step 1 + if (code && !userSetCode(code)) { + throw new Error('InvalidAccessError: Invalid code'); + } + // Step 2.4 states the max is 123 bytes, but we are just checking length + if (reason && reason.length > 123) { + throw new SyntaxError('reason argument has an invalid length'); + } + + // Step 3.1 + if (this.readyState === SockJS.CLOSING || this.readyState === SockJS.CLOSED) { + return; + } + + // TODO look at docs to determine how to set this + var wasClean = true; + this._close(code || 1000, reason || 'Normal closure', wasClean); +}; + +SockJS.prototype.send = function(data) { + // #13 - convert anything non-string to string + // TODO this currently turns objects into [object Object] + if (typeof data !== 'string') { + data = '' + data; + } + if (this.readyState === SockJS.CONNECTING) { + throw new Error('InvalidStateError: The connection has not been established yet'); + } + if (this.readyState !== SockJS.OPEN) { + return; + } + this._transport.send(escape.quote(data)); +}; + +SockJS.version = require('./version'); + +SockJS.CONNECTING = 0; +SockJS.OPEN = 1; +SockJS.CLOSING = 2; +SockJS.CLOSED = 3; + +SockJS.prototype._receiveInfo = function(info, rtt) { + debug('_receiveInfo', rtt); + this._ir = null; + if (!info) { + this._close(1002, 'Cannot connect to server'); + return; + } + + // establish a round-trip timeout (RTO) based on the + // round-trip time (RTT) + this._rto = this.countRTO(rtt); + // allow server to override url used for the actual transport + this._transUrl = info.base_url ? info.base_url : this.url; + info = objectUtils.extend(info, this._urlInfo); + debug('info', info); + // determine list of desired and supported transports + var enabledTransports = transports.filterToEnabled(this._transportsWhitelist, info); + this._transports = enabledTransports.main; + debug(this._transports.length + ' enabled transports'); + + this._connect(); +}; + +SockJS.prototype._connect = function() { + for (var Transport = this._transports.shift(); Transport; Transport = this._transports.shift()) { + debug('attempt', Transport.transportName); + if (Transport.needBody) { + if (!global.document.body || + (typeof global.document.readyState !== 'undefined' && + global.document.readyState !== 'complete' && + global.document.readyState !== 'interactive')) { + debug('waiting for body'); + this._transports.unshift(Transport); + eventUtils.attachEvent('load', this._connect.bind(this)); + return; + } + } + + // calculate timeout based on RTO and round trips. Default to 5s + var timeoutMs = (this._rto * Transport.roundTrips) || 5000; + this._transportTimeoutId = setTimeout(this._transportTimeout.bind(this), timeoutMs); + debug('using timeout', timeoutMs); + + var transportUrl = urlUtils.addPath(this._transUrl, '/' + this._server + '/' + this._generateSessionId()); + var options = this._transportOptions[Transport.transportName]; + debug('transport url', transportUrl); + var transportObj = new Transport(transportUrl, this._transUrl, options); + transportObj.on('message', this._transportMessage.bind(this)); + transportObj.once('close', this._transportClose.bind(this)); + transportObj.transportName = Transport.transportName; + this._transport = transportObj; + + return; + } + this._close(2000, 'All transports failed', false); +}; + +SockJS.prototype._transportTimeout = function() { + debug('_transportTimeout'); + if (this.readyState === SockJS.CONNECTING) { + this._transportClose(2007, 'Transport timed out'); + } +}; + +SockJS.prototype._transportMessage = function(msg) { + debug('_transportMessage', msg); + var self = this + , type = msg.slice(0, 1) + , content = msg.slice(1) + , payload + ; + + // first check for messages that don't need a payload + switch (type) { + case 'o': + this._open(); + return; + case 'h': + this.dispatchEvent(new Event('heartbeat')); + debug('heartbeat', this.transport); + return; + } + + if (content) { + try { + payload = JSON3.parse(content); + } catch (e) { + debug('bad json', content); + } + } + + if (typeof payload === 'undefined') { + debug('empty payload', content); + return; + } + + switch (type) { + case 'a': + if (Array.isArray(payload)) { + payload.forEach(function(p) { + debug('message', self.transport, p); + self.dispatchEvent(new TransportMessageEvent(p)); + }); + } + break; + case 'm': + debug('message', this.transport, payload); + this.dispatchEvent(new TransportMessageEvent(payload)); + break; + case 'c': + if (Array.isArray(payload) && payload.length === 2) { + this._close(payload[0], payload[1], true); + } + break; + } +}; + +SockJS.prototype._transportClose = function(code, reason) { + debug('_transportClose', this.transport, code, reason); + if (this._transport) { + this._transport.removeAllListeners(); + this._transport = null; + this.transport = null; + } + + if (!userSetCode(code) && code !== 2000 && this.readyState === SockJS.CONNECTING) { + this._connect(); + return; + } + + this._close(code, reason); +}; + +SockJS.prototype._open = function() { + debug('_open', this._transport.transportName, this.readyState); + if (this.readyState === SockJS.CONNECTING) { + if (this._transportTimeoutId) { + clearTimeout(this._transportTimeoutId); + this._transportTimeoutId = null; + } + this.readyState = SockJS.OPEN; + this.transport = this._transport.transportName; + this.dispatchEvent(new Event('open')); + debug('connected', this.transport); + } else { + // The server might have been restarted, and lost track of our + // connection. + this._close(1006, 'Server lost session'); + } +}; + +SockJS.prototype._close = function(code, reason, wasClean) { + debug('_close', this.transport, code, reason, wasClean, this.readyState); + var forceFail = false; + + if (this._ir) { + forceFail = true; + this._ir.close(); + this._ir = null; + } + if (this._transport) { + this._transport.close(); + this._transport = null; + this.transport = null; + } + + if (this.readyState === SockJS.CLOSED) { + throw new Error('InvalidStateError: SockJS has already been closed'); + } + + this.readyState = SockJS.CLOSING; + setTimeout(function() { + this.readyState = SockJS.CLOSED; + + if (forceFail) { + this.dispatchEvent(new Event('error')); + } + + var e = new CloseEvent('close'); + e.wasClean = wasClean || false; + e.code = code || 1000; + e.reason = reason; + + this.dispatchEvent(e); + this.onmessage = this.onclose = this.onerror = null; + debug('disconnected'); + }.bind(this), 0); +}; + +// See: http://www.erg.abdn.ac.uk/~gerrit/dccp/notes/ccid2/rto_estimator/ +// and RFC 2988. +SockJS.prototype.countRTO = function(rtt) { + // In a local environment, when using IE8/9 and the `jsonp-polling` + // transport the time needed to establish a connection (the time that pass + // from the opening of the transport to the call of `_dispatchOpen`) is + // around 200msec (the lower bound used in the article above) and this + // causes spurious timeouts. For this reason we calculate a value slightly + // larger than that used in the article. + if (rtt > 100) { + return 4 * rtt; // rto > 400msec + } + return 300 + rtt; // 300msec < rto <= 400msec +}; + +module.exports = function(availableTransports) { + transports = transport(availableTransports); + require('./iframe-bootstrap')(SockJS, availableTransports); + return SockJS; +}; + +}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./event/close":35,"./event/event":37,"./event/eventtarget":38,"./event/trans-message":39,"./iframe-bootstrap":41,"./info-receiver":45,"./location":46,"./shims":48,"./utils/browser":77,"./utils/escape":78,"./utils/event":79,"./utils/log":81,"./utils/object":82,"./utils/random":83,"./utils/transport":84,"./utils/url":85,"./version":86,"_process":110,"debug":1,"inherits":7,"json3":8,"url-parse":88}],48:[function(require,module,exports){ +/* eslint-disable */ +/* jscs: disable */ +'use strict'; + +// pulled specific shims from https://github.com/es-shims/es5-shim + +var ArrayPrototype = Array.prototype; +var ObjectPrototype = Object.prototype; +var FunctionPrototype = Function.prototype; +var StringPrototype = String.prototype; +var array_slice = ArrayPrototype.slice; + +var _toString = ObjectPrototype.toString; +var isFunction = function (val) { + return ObjectPrototype.toString.call(val) === '[object Function]'; +}; +var isArray = function isArray(obj) { + return _toString.call(obj) === '[object Array]'; +}; +var isString = function isString(obj) { + return _toString.call(obj) === '[object String]'; +}; + +var supportsDescriptors = Object.defineProperty && (function () { + try { + Object.defineProperty({}, 'x', {}); + return true; + } catch (e) { /* this is ES3 */ + return false; + } +}()); + +// Define configurable, writable and non-enumerable props +// if they don't exist. +var defineProperty; +if (supportsDescriptors) { + defineProperty = function (object, name, method, forceAssign) { + if (!forceAssign && (name in object)) { return; } + Object.defineProperty(object, name, { + configurable: true, + enumerable: false, + writable: true, + value: method + }); + }; +} else { + defineProperty = function (object, name, method, forceAssign) { + if (!forceAssign && (name in object)) { return; } + object[name] = method; + }; +} +var defineProperties = function (object, map, forceAssign) { + for (var name in map) { + if (ObjectPrototype.hasOwnProperty.call(map, name)) { + defineProperty(object, name, map[name], forceAssign); + } + } +}; + +var toObject = function (o) { + if (o == null) { // this matches both null and undefined + throw new TypeError("can't convert " + o + ' to object'); + } + return Object(o); +}; + +// +// Util +// ====== +// + +// ES5 9.4 +// http://es5.github.com/#x9.4 +// http://jsperf.com/to-integer + +function toInteger(num) { + var n = +num; + if (n !== n) { // isNaN + n = 0; + } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + return n; +} + +function ToUint32(x) { + return x >>> 0; +} + +// +// Function +// ======== +// + +// ES-5 15.3.4.5 +// http://es5.github.com/#x15.3.4.5 + +function Empty() {} + +defineProperties(FunctionPrototype, { + bind: function bind(that) { // .length is 1 + // 1. Let Target be the this value. + var target = this; + // 2. If IsCallable(Target) is false, throw a TypeError exception. + if (!isFunction(target)) { + throw new TypeError('Function.prototype.bind called on incompatible ' + target); + } + // 3. Let A be a new (possibly empty) internal list of all of the + // argument values provided after thisArg (arg1, arg2 etc), in order. + // XXX slicedArgs will stand in for "A" if used + var args = array_slice.call(arguments, 1); // for normal call + // 4. Let F be a new native ECMAScript object. + // 11. Set the [[Prototype]] internal property of F to the standard + // built-in Function prototype object as specified in 15.3.3.1. + // 12. Set the [[Call]] internal property of F as described in + // 15.3.4.5.1. + // 13. Set the [[Construct]] internal property of F as described in + // 15.3.4.5.2. + // 14. Set the [[HasInstance]] internal property of F as described in + // 15.3.4.5.3. + var binder = function () { + + if (this instanceof bound) { + // 15.3.4.5.2 [[Construct]] + // When the [[Construct]] internal method of a function object, + // F that was created using the bind function is called with a + // list of arguments ExtraArgs, the following steps are taken: + // 1. Let target be the value of F's [[TargetFunction]] + // internal property. + // 2. If target has no [[Construct]] internal method, a + // TypeError exception is thrown. + // 3. Let boundArgs be the value of F's [[BoundArgs]] internal + // property. + // 4. Let args be a new list containing the same values as the + // list boundArgs in the same order followed by the same + // values as the list ExtraArgs in the same order. + // 5. Return the result of calling the [[Construct]] internal + // method of target providing args as the arguments. + + var result = target.apply( + this, + args.concat(array_slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return this; + + } else { + // 15.3.4.5.1 [[Call]] + // When the [[Call]] internal method of a function object, F, + // which was created using the bind function is called with a + // this value and a list of arguments ExtraArgs, the following + // steps are taken: + // 1. Let boundArgs be the value of F's [[BoundArgs]] internal + // property. + // 2. Let boundThis be the value of F's [[BoundThis]] internal + // property. + // 3. Let target be the value of F's [[TargetFunction]] internal + // property. + // 4. Let args be a new list containing the same values as the + // list boundArgs in the same order followed by the same + // values as the list ExtraArgs in the same order. + // 5. Return the result of calling the [[Call]] internal method + // of target providing boundThis as the this value and + // providing args as the arguments. + + // equiv: target.call(this, ...boundArgs, ...args) + return target.apply( + that, + args.concat(array_slice.call(arguments)) + ); + + } + + }; + + // 15. If the [[Class]] internal property of Target is "Function", then + // a. Let L be the length property of Target minus the length of A. + // b. Set the length own property of F to either 0 or L, whichever is + // larger. + // 16. Else set the length own property of F to 0. + + var boundLength = Math.max(0, target.length - args.length); + + // 17. Set the attributes of the length own property of F to the values + // specified in 15.3.5.1. + var boundArgs = []; + for (var i = 0; i < boundLength; i++) { + boundArgs.push('$' + i); + } + + // XXX Build a dynamic function with desired amount of arguments is the only + // way to set the length property of a function. + // In environments where Content Security Policies enabled (Chrome extensions, + // for ex.) all use of eval or Function costructor throws an exception. + // However in all of these environments Function.prototype.bind exists + // and so this code will never be executed. + var bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder); + + if (target.prototype) { + Empty.prototype = target.prototype; + bound.prototype = new Empty(); + // Clean up dangling references. + Empty.prototype = null; + } + + // TODO + // 18. Set the [[Extensible]] internal property of F to true. + + // TODO + // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3). + // 20. Call the [[DefineOwnProperty]] internal method of F with + // arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]: + // thrower, [[Enumerable]]: false, [[Configurable]]: false}, and + // false. + // 21. Call the [[DefineOwnProperty]] internal method of F with + // arguments "arguments", PropertyDescriptor {[[Get]]: thrower, + // [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, + // and false. + + // TODO + // NOTE Function objects created using Function.prototype.bind do not + // have a prototype property or the [[Code]], [[FormalParameters]], and + // [[Scope]] internal properties. + // XXX can't delete prototype in pure-js. + + // 22. Return F. + return bound; + } +}); + +// +// Array +// ===== +// + +// ES5 15.4.3.2 +// http://es5.github.com/#x15.4.3.2 +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray +defineProperties(Array, { isArray: isArray }); + + +var boxedString = Object('a'); +var splitString = boxedString[0] !== 'a' || !(0 in boxedString); + +var properlyBoxesContext = function properlyBoxed(method) { + // Check node 0.6.21 bug where third parameter is not boxed + var properlyBoxesNonStrict = true; + var properlyBoxesStrict = true; + if (method) { + method.call('foo', function (_, __, context) { + if (typeof context !== 'object') { properlyBoxesNonStrict = false; } + }); + + method.call([1], function () { + 'use strict'; + properlyBoxesStrict = typeof this === 'string'; + }, 'x'); + } + return !!method && properlyBoxesNonStrict && properlyBoxesStrict; +}; + +defineProperties(ArrayPrototype, { + forEach: function forEach(fun /*, thisp*/) { + var object = toObject(this), + self = splitString && isString(this) ? this.split('') : object, + thisp = arguments[1], + i = -1, + length = self.length >>> 0; + + // If no callback function or if callback is not a callable function + if (!isFunction(fun)) { + throw new TypeError(); // TODO message + } + + while (++i < length) { + if (i in self) { + // Invoke the callback function with call, passing arguments: + // context, property value, property key, thisArg object + // context + fun.call(thisp, self[i], i, object); + } + } + } +}, !properlyBoxesContext(ArrayPrototype.forEach)); + +// ES5 15.4.4.14 +// http://es5.github.com/#x15.4.4.14 +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf +var hasFirefox2IndexOfBug = Array.prototype.indexOf && [0, 1].indexOf(1, 2) !== -1; +defineProperties(ArrayPrototype, { + indexOf: function indexOf(sought /*, fromIndex */ ) { + var self = splitString && isString(this) ? this.split('') : toObject(this), + length = self.length >>> 0; + + if (!length) { + return -1; + } + + var i = 0; + if (arguments.length > 1) { + i = toInteger(arguments[1]); + } + + // handle negative indices + i = i >= 0 ? i : Math.max(0, length + i); + for (; i < length; i++) { + if (i in self && self[i] === sought) { + return i; + } + } + return -1; + } +}, hasFirefox2IndexOfBug); + +// +// String +// ====== +// + +// ES5 15.5.4.14 +// http://es5.github.com/#x15.5.4.14 + +// [bugfix, IE lt 9, firefox 4, Konqueror, Opera, obscure browsers] +// Many browsers do not split properly with regular expressions or they +// do not perform the split correctly under obscure conditions. +// See http://blog.stevenlevithan.com/archives/cross-browser-split +// I've tested in many browsers and this seems to cover the deviant ones: +// 'ab'.split(/(?:ab)*/) should be ["", ""], not [""] +// '.'.split(/(.?)(.?)/) should be ["", ".", "", ""], not ["", ""] +// 'tesst'.split(/(s)*/) should be ["t", undefined, "e", "s", "t"], not +// [undefined, "t", undefined, "e", ...] +// ''.split(/.?/) should be [], not [""] +// '.'.split(/()()/) should be ["."], not ["", "", "."] + +var string_split = StringPrototype.split; +if ( + 'ab'.split(/(?:ab)*/).length !== 2 || + '.'.split(/(.?)(.?)/).length !== 4 || + 'tesst'.split(/(s)*/)[1] === 't' || + 'test'.split(/(?:)/, -1).length !== 4 || + ''.split(/.?/).length || + '.'.split(/()()/).length > 1 +) { + (function () { + var compliantExecNpcg = /()??/.exec('')[1] === void 0; // NPCG: nonparticipating capturing group + + StringPrototype.split = function (separator, limit) { + var string = this; + if (separator === void 0 && limit === 0) { + return []; + } + + // If `separator` is not a regex, use native split + if (_toString.call(separator) !== '[object RegExp]') { + return string_split.call(this, separator, limit); + } + + var output = [], + flags = (separator.ignoreCase ? 'i' : '') + + (separator.multiline ? 'm' : '') + + (separator.extended ? 'x' : '') + // Proposed for ES6 + (separator.sticky ? 'y' : ''), // Firefox 3+ + lastLastIndex = 0, + // Make `global` and avoid `lastIndex` issues by working with a copy + separator2, match, lastIndex, lastLength; + separator = new RegExp(separator.source, flags + 'g'); + string += ''; // Type-convert + if (!compliantExecNpcg) { + // Doesn't need flags gy, but they don't hurt + separator2 = new RegExp('^' + separator.source + '$(?!\\s)', flags); + } + /* Values for `limit`, per the spec: + * If undefined: 4294967295 // Math.pow(2, 32) - 1 + * If 0, Infinity, or NaN: 0 + * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296; + * If negative number: 4294967296 - Math.floor(Math.abs(limit)) + * If other: Type-convert, then use the above rules + */ + limit = limit === void 0 ? + -1 >>> 0 : // Math.pow(2, 32) - 1 + ToUint32(limit); + while (match = separator.exec(string)) { + // `separator.lastIndex` is not reliable cross-browser + lastIndex = match.index + match[0].length; + if (lastIndex > lastLastIndex) { + output.push(string.slice(lastLastIndex, match.index)); + // Fix browsers whose `exec` methods don't consistently return `undefined` for + // nonparticipating capturing groups + if (!compliantExecNpcg && match.length > 1) { + match[0].replace(separator2, function () { + for (var i = 1; i < arguments.length - 2; i++) { + if (arguments[i] === void 0) { + match[i] = void 0; + } + } + }); + } + if (match.length > 1 && match.index < string.length) { + ArrayPrototype.push.apply(output, match.slice(1)); + } + lastLength = match[0].length; + lastLastIndex = lastIndex; + if (output.length >= limit) { + break; + } + } + if (separator.lastIndex === match.index) { + separator.lastIndex++; // Avoid an infinite loop + } + } + if (lastLastIndex === string.length) { + if (lastLength || !separator.test('')) { + output.push(''); + } + } else { + output.push(string.slice(lastLastIndex)); + } + return output.length > limit ? output.slice(0, limit) : output; + }; + }()); + +// [bugfix, chrome] +// If separator is undefined, then the result array contains just one String, +// which is the this value (converted to a String). If limit is not undefined, +// then the output array is truncated so that it contains no more than limit +// elements. +// "0".split(undefined, 0) -> [] +} else if ('0'.split(void 0, 0).length) { + StringPrototype.split = function split(separator, limit) { + if (separator === void 0 && limit === 0) { return []; } + return string_split.call(this, separator, limit); + }; +} + +// ES5 15.5.4.20 +// whitespace from: http://es5.github.io/#x15.5.4.20 +var ws = '\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003' + + '\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028' + + '\u2029\uFEFF'; +var zeroWidth = '\u200b'; +var wsRegexChars = '[' + ws + ']'; +var trimBeginRegexp = new RegExp('^' + wsRegexChars + wsRegexChars + '*'); +var trimEndRegexp = new RegExp(wsRegexChars + wsRegexChars + '*$'); +var hasTrimWhitespaceBug = StringPrototype.trim && (ws.trim() || !zeroWidth.trim()); +defineProperties(StringPrototype, { + // http://blog.stevenlevithan.com/archives/faster-trim-javascript + // http://perfectionkills.com/whitespace-deviations/ + trim: function trim() { + if (this === void 0 || this === null) { + throw new TypeError("can't convert " + this + ' to object'); + } + return String(this).replace(trimBeginRegexp, '').replace(trimEndRegexp, ''); + } +}, hasTrimWhitespaceBug); + +// ECMA-262, 3rd B.2.3 +// Not an ECMAScript standard, although ECMAScript 3rd Edition has a +// non-normative section suggesting uniform semantics and it should be +// normalized across all browsers +// [bugfix, IE lt 9] IE < 9 substr() with negative value not working in IE +var string_substr = StringPrototype.substr; +var hasNegativeSubstrBug = ''.substr && '0b'.substr(-1) !== 'b'; +defineProperties(StringPrototype, { + substr: function substr(start, length) { + return string_substr.call( + this, + start < 0 ? ((start = this.length + start) < 0 ? 0 : start) : start, + length + ); + } +}, hasNegativeSubstrBug); + +},{}],49:[function(require,module,exports){ +'use strict'; + +module.exports = [ + // streaming transports + require('./transport/websocket') +, require('./transport/xhr-streaming') +, require('./transport/xdr-streaming') +, require('./transport/eventsource') +, require('./transport/lib/iframe-wrap')(require('./transport/eventsource')) + + // polling transports +, require('./transport/htmlfile') +, require('./transport/lib/iframe-wrap')(require('./transport/htmlfile')) +, require('./transport/xhr-polling') +, require('./transport/xdr-polling') +, require('./transport/lib/iframe-wrap')(require('./transport/xhr-polling')) +, require('./transport/jsonp-polling') +]; + +},{"./transport/eventsource":53,"./transport/htmlfile":54,"./transport/jsonp-polling":56,"./transport/lib/iframe-wrap":59,"./transport/websocket":71,"./transport/xdr-polling":72,"./transport/xdr-streaming":73,"./transport/xhr-polling":74,"./transport/xhr-streaming":75}],50:[function(require,module,exports){ +(function (process,global){ +'use strict'; + +var EventEmitter = require('events').EventEmitter + , inherits = require('inherits') + , utils = require('../../utils/event') + , urlUtils = require('../../utils/url') + , XHR = global.XMLHttpRequest + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:browser:xhr'); +} + +function AbstractXHRObject(method, url, payload, opts) { + debug(method, url); + var self = this; + EventEmitter.call(this); + + setTimeout(function () { + self._start(method, url, payload, opts); + }, 0); +} + +inherits(AbstractXHRObject, EventEmitter); + +AbstractXHRObject.prototype._start = function(method, url, payload, opts) { + var self = this; + + try { + this.xhr = new XHR(); + } catch (x) { + // intentionally empty + } + + if (!this.xhr) { + debug('no xhr'); + this.emit('finish', 0, 'no xhr support'); + this._cleanup(); + return; + } + + // several browsers cache POSTs + url = urlUtils.addQuery(url, 't=' + (+new Date())); + + // Explorer tends to keep connection open, even after the + // tab gets closed: http://bugs.jquery.com/ticket/5280 + this.unloadRef = utils.unloadAdd(function() { + debug('unload cleanup'); + self._cleanup(true); + }); + try { + this.xhr.open(method, url, true); + if (this.timeout && 'timeout' in this.xhr) { + this.xhr.timeout = this.timeout; + this.xhr.ontimeout = function() { + debug('xhr timeout'); + self.emit('finish', 0, ''); + self._cleanup(false); + }; + } + } catch (e) { + debug('exception', e); + // IE raises an exception on wrong port. + this.emit('finish', 0, ''); + this._cleanup(false); + return; + } + + if ((!opts || !opts.noCredentials) && AbstractXHRObject.supportsCORS) { + debug('withCredentials'); + // Mozilla docs says https://developer.mozilla.org/en/XMLHttpRequest : + // "This never affects same-site requests." + + this.xhr.withCredentials = 'true'; + } + if (opts && opts.headers) { + for (var key in opts.headers) { + this.xhr.setRequestHeader(key, opts.headers[key]); + } + } + + this.xhr.onreadystatechange = function() { + if (self.xhr) { + var x = self.xhr; + var text, status; + debug('readyState', x.readyState); + switch (x.readyState) { + case 3: + // IE doesn't like peeking into responseText or status + // on Microsoft.XMLHTTP and readystate=3 + try { + status = x.status; + text = x.responseText; + } catch (e) { + // intentionally empty + } + debug('status', status); + // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 + if (status === 1223) { + status = 204; + } + + // IE does return readystate == 3 for 404 answers. + if (status === 200 && text && text.length > 0) { + debug('chunk'); + self.emit('chunk', status, text); + } + break; + case 4: + status = x.status; + debug('status', status); + // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 + if (status === 1223) { + status = 204; + } + // IE returns this for a bad port + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa383770(v=vs.85).aspx + if (status === 12005 || status === 12029) { + status = 0; + } + + debug('finish', status, x.responseText); + self.emit('finish', status, x.responseText); + self._cleanup(false); + break; + } + } + }; + + try { + self.xhr.send(payload); + } catch (e) { + self.emit('finish', 0, ''); + self._cleanup(false); + } +}; + +AbstractXHRObject.prototype._cleanup = function(abort) { + debug('cleanup'); + if (!this.xhr) { + return; + } + this.removeAllListeners(); + utils.unloadDel(this.unloadRef); + + // IE needs this field to be a function + this.xhr.onreadystatechange = function() {}; + if (this.xhr.ontimeout) { + this.xhr.ontimeout = null; + } + + if (abort) { + try { + this.xhr.abort(); + } catch (x) { + // intentionally empty + } + } + this.unloadRef = this.xhr = null; +}; + +AbstractXHRObject.prototype.close = function() { + debug('close'); + this._cleanup(true); +}; + +AbstractXHRObject.enabled = !!XHR; +// override XMLHttpRequest for IE6/7 +// obfuscate to avoid firewalls +var axo = ['Active'].concat('Object').join('X'); +if (!AbstractXHRObject.enabled && (axo in global)) { + debug('overriding xmlhttprequest'); + XHR = function() { + try { + return new global[axo]('Microsoft.XMLHTTP'); + } catch (e) { + return null; + } + }; + AbstractXHRObject.enabled = !!new XHR(); +} + +var cors = false; +try { + cors = 'withCredentials' in new XHR(); +} catch (ignored) { + // intentionally empty +} + +AbstractXHRObject.supportsCORS = cors; + +module.exports = AbstractXHRObject; + +}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"../../utils/event":79,"../../utils/url":85,"_process":110,"debug":1,"events":36,"inherits":7}],51:[function(require,module,exports){ +(function (global){ +module.exports = global.EventSource; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{}],52:[function(require,module,exports){ +(function (global){ +'use strict'; + +var Driver = global.WebSocket || global.MozWebSocket; +if (Driver) { + module.exports = function WebSocketBrowserDriver(url) { + return new Driver(url); + }; +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{}],53:[function(require,module,exports){ +'use strict'; + +var inherits = require('inherits') + , AjaxBasedTransport = require('./lib/ajax-based') + , EventSourceReceiver = require('./receiver/eventsource') + , XHRCorsObject = require('./sender/xhr-cors') + , EventSourceDriver = require('eventsource') + ; + +function EventSourceTransport(transUrl) { + if (!EventSourceTransport.enabled()) { + throw new Error('Transport created when disabled'); + } + + AjaxBasedTransport.call(this, transUrl, '/eventsource', EventSourceReceiver, XHRCorsObject); +} + +inherits(EventSourceTransport, AjaxBasedTransport); + +EventSourceTransport.enabled = function() { + return !!EventSourceDriver; +}; + +EventSourceTransport.transportName = 'eventsource'; +EventSourceTransport.roundTrips = 2; + +module.exports = EventSourceTransport; + +},{"./lib/ajax-based":57,"./receiver/eventsource":62,"./sender/xhr-cors":68,"eventsource":51,"inherits":7}],54:[function(require,module,exports){ +'use strict'; + +var inherits = require('inherits') + , HtmlfileReceiver = require('./receiver/htmlfile') + , XHRLocalObject = require('./sender/xhr-local') + , AjaxBasedTransport = require('./lib/ajax-based') + ; + +function HtmlFileTransport(transUrl) { + if (!HtmlfileReceiver.enabled) { + throw new Error('Transport created when disabled'); + } + AjaxBasedTransport.call(this, transUrl, '/htmlfile', HtmlfileReceiver, XHRLocalObject); +} + +inherits(HtmlFileTransport, AjaxBasedTransport); + +HtmlFileTransport.enabled = function(info) { + return HtmlfileReceiver.enabled && info.sameOrigin; +}; + +HtmlFileTransport.transportName = 'htmlfile'; +HtmlFileTransport.roundTrips = 2; + +module.exports = HtmlFileTransport; + +},{"./lib/ajax-based":57,"./receiver/htmlfile":63,"./sender/xhr-local":70,"inherits":7}],55:[function(require,module,exports){ +(function (process){ +'use strict'; + +// Few cool transports do work only for same-origin. In order to make +// them work cross-domain we shall use iframe, served from the +// remote domain. New browsers have capabilities to communicate with +// cross domain iframe using postMessage(). In IE it was implemented +// from IE 8+, but of course, IE got some details wrong: +// http://msdn.microsoft.com/en-us/library/cc197015(v=VS.85).aspx +// http://stevesouders.com/misc/test-postmessage.php + +var inherits = require('inherits') + , JSON3 = require('json3') + , EventEmitter = require('events').EventEmitter + , version = require('../version') + , urlUtils = require('../utils/url') + , iframeUtils = require('../utils/iframe') + , eventUtils = require('../utils/event') + , random = require('../utils/random') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:transport:iframe'); +} + +function IframeTransport(transport, transUrl, baseUrl) { + if (!IframeTransport.enabled()) { + throw new Error('Transport created when disabled'); + } + EventEmitter.call(this); + + var self = this; + this.origin = urlUtils.getOrigin(baseUrl); + this.baseUrl = baseUrl; + this.transUrl = transUrl; + this.transport = transport; + this.windowId = random.string(8); + + var iframeUrl = urlUtils.addPath(baseUrl, '/iframe.html') + '#' + this.windowId; + debug(transport, transUrl, iframeUrl); + + this.iframeObj = iframeUtils.createIframe(iframeUrl, function(r) { + debug('err callback'); + self.emit('close', 1006, 'Unable to load an iframe (' + r + ')'); + self.close(); + }); + + this.onmessageCallback = this._message.bind(this); + eventUtils.attachEvent('message', this.onmessageCallback); +} + +inherits(IframeTransport, EventEmitter); + +IframeTransport.prototype.close = function() { + debug('close'); + this.removeAllListeners(); + if (this.iframeObj) { + eventUtils.detachEvent('message', this.onmessageCallback); + try { + // When the iframe is not loaded, IE raises an exception + // on 'contentWindow'. + this.postMessage('c'); + } catch (x) { + // intentionally empty + } + this.iframeObj.cleanup(); + this.iframeObj = null; + this.onmessageCallback = this.iframeObj = null; + } +}; + +IframeTransport.prototype._message = function(e) { + debug('message', e.data); + if (!urlUtils.isOriginEqual(e.origin, this.origin)) { + debug('not same origin', e.origin, this.origin); + return; + } + + var iframeMessage; + try { + iframeMessage = JSON3.parse(e.data); + } catch (ignored) { + debug('bad json', e.data); + return; + } + + if (iframeMessage.windowId !== this.windowId) { + debug('mismatched window id', iframeMessage.windowId, this.windowId); + return; + } + + switch (iframeMessage.type) { + case 's': + this.iframeObj.loaded(); + // window global dependency + this.postMessage('s', JSON3.stringify([ + version + , this.transport + , this.transUrl + , this.baseUrl + ])); + break; + case 't': + this.emit('message', iframeMessage.data); + break; + case 'c': + var cdata; + try { + cdata = JSON3.parse(iframeMessage.data); + } catch (ignored) { + debug('bad json', iframeMessage.data); + return; + } + this.emit('close', cdata[0], cdata[1]); + this.close(); + break; + } +}; + +IframeTransport.prototype.postMessage = function(type, data) { + debug('postMessage', type, data); + this.iframeObj.post(JSON3.stringify({ + windowId: this.windowId + , type: type + , data: data || '' + }), this.origin); +}; + +IframeTransport.prototype.send = function(message) { + debug('send', message); + this.postMessage('m', message); +}; + +IframeTransport.enabled = function() { + return iframeUtils.iframeEnabled; +}; + +IframeTransport.transportName = 'iframe'; +IframeTransport.roundTrips = 2; + +module.exports = IframeTransport; + +}).call(this,require('_process')) + +},{"../utils/event":79,"../utils/iframe":80,"../utils/random":83,"../utils/url":85,"../version":86,"_process":110,"debug":1,"events":36,"inherits":7,"json3":8}],56:[function(require,module,exports){ +(function (global){ +'use strict'; + +// The simplest and most robust transport, using the well-know cross +// domain hack - JSONP. This transport is quite inefficient - one +// message could use up to one http request. But at least it works almost +// everywhere. +// Known limitations: +// o you will get a spinning cursor +// o for Konqueror a dumb timer is needed to detect errors + +var inherits = require('inherits') + , SenderReceiver = require('./lib/sender-receiver') + , JsonpReceiver = require('./receiver/jsonp') + , jsonpSender = require('./sender/jsonp') + ; + +function JsonPTransport(transUrl) { + if (!JsonPTransport.enabled()) { + throw new Error('Transport created when disabled'); + } + SenderReceiver.call(this, transUrl, '/jsonp', jsonpSender, JsonpReceiver); +} + +inherits(JsonPTransport, SenderReceiver); + +JsonPTransport.enabled = function() { + return !!global.document; +}; + +JsonPTransport.transportName = 'jsonp-polling'; +JsonPTransport.roundTrips = 1; +JsonPTransport.needBody = true; + +module.exports = JsonPTransport; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./lib/sender-receiver":61,"./receiver/jsonp":64,"./sender/jsonp":66,"inherits":7}],57:[function(require,module,exports){ +(function (process){ +'use strict'; + +var inherits = require('inherits') + , urlUtils = require('../../utils/url') + , SenderReceiver = require('./sender-receiver') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:ajax-based'); +} + +function createAjaxSender(AjaxObject) { + return function(url, payload, callback) { + debug('create ajax sender', url, payload); + var opt = {}; + if (typeof payload === 'string') { + opt.headers = {'Content-type': 'text/plain'}; + } + var ajaxUrl = urlUtils.addPath(url, '/xhr_send'); + var xo = new AjaxObject('POST', ajaxUrl, payload, opt); + xo.once('finish', function(status) { + debug('finish', status); + xo = null; + + if (status !== 200 && status !== 204) { + return callback(new Error('http status ' + status)); + } + callback(); + }); + return function() { + debug('abort'); + xo.close(); + xo = null; + + var err = new Error('Aborted'); + err.code = 1000; + callback(err); + }; + }; +} + +function AjaxBasedTransport(transUrl, urlSuffix, Receiver, AjaxObject) { + SenderReceiver.call(this, transUrl, urlSuffix, createAjaxSender(AjaxObject), Receiver, AjaxObject); +} + +inherits(AjaxBasedTransport, SenderReceiver); + +module.exports = AjaxBasedTransport; + +}).call(this,require('_process')) + +},{"../../utils/url":85,"./sender-receiver":61,"_process":110,"debug":1,"inherits":7}],58:[function(require,module,exports){ +(function (process){ +'use strict'; + +var inherits = require('inherits') + , EventEmitter = require('events').EventEmitter + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:buffered-sender'); +} + +function BufferedSender(url, sender) { + debug(url); + EventEmitter.call(this); + this.sendBuffer = []; + this.sender = sender; + this.url = url; +} + +inherits(BufferedSender, EventEmitter); + +BufferedSender.prototype.send = function(message) { + debug('send', message); + this.sendBuffer.push(message); + if (!this.sendStop) { + this.sendSchedule(); + } +}; + +// For polling transports in a situation when in the message callback, +// new message is being send. If the sending connection was started +// before receiving one, it is possible to saturate the network and +// timeout due to the lack of receiving socket. To avoid that we delay +// sending messages by some small time, in order to let receiving +// connection be started beforehand. This is only a halfmeasure and +// does not fix the big problem, but it does make the tests go more +// stable on slow networks. +BufferedSender.prototype.sendScheduleWait = function() { + debug('sendScheduleWait'); + var self = this; + var tref; + this.sendStop = function() { + debug('sendStop'); + self.sendStop = null; + clearTimeout(tref); + }; + tref = setTimeout(function() { + debug('timeout'); + self.sendStop = null; + self.sendSchedule(); + }, 25); +}; + +BufferedSender.prototype.sendSchedule = function() { + debug('sendSchedule', this.sendBuffer.length); + var self = this; + if (this.sendBuffer.length > 0) { + var payload = '[' + this.sendBuffer.join(',') + ']'; + this.sendStop = this.sender(this.url, payload, function(err) { + self.sendStop = null; + if (err) { + debug('error', err); + self.emit('close', err.code || 1006, 'Sending error: ' + err); + self._cleanup(); + } else { + self.sendScheduleWait(); + } + }); + this.sendBuffer = []; + } +}; + +BufferedSender.prototype._cleanup = function() { + debug('_cleanup'); + this.removeAllListeners(); +}; + +BufferedSender.prototype.stop = function() { + debug('stop'); + this._cleanup(); + if (this.sendStop) { + this.sendStop(); + this.sendStop = null; + } +}; + +module.exports = BufferedSender; + +}).call(this,require('_process')) + +},{"_process":110,"debug":1,"events":36,"inherits":7}],59:[function(require,module,exports){ +(function (global){ +'use strict'; + +var inherits = require('inherits') + , IframeTransport = require('../iframe') + , objectUtils = require('../../utils/object') + ; + +module.exports = function(transport) { + + function IframeWrapTransport(transUrl, baseUrl) { + IframeTransport.call(this, transport.transportName, transUrl, baseUrl); + } + + inherits(IframeWrapTransport, IframeTransport); + + IframeWrapTransport.enabled = function(url, info) { + if (!global.document) { + return false; + } + + var iframeInfo = objectUtils.extend({}, info); + iframeInfo.sameOrigin = true; + return transport.enabled(iframeInfo) && IframeTransport.enabled(); + }; + + IframeWrapTransport.transportName = 'iframe-' + transport.transportName; + IframeWrapTransport.needBody = true; + IframeWrapTransport.roundTrips = IframeTransport.roundTrips + transport.roundTrips - 1; // html, javascript (2) + transport - no CORS (1) + + IframeWrapTransport.facadeTransport = transport; + + return IframeWrapTransport; +}; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"../../utils/object":82,"../iframe":55,"inherits":7}],60:[function(require,module,exports){ +(function (process){ +'use strict'; + +var inherits = require('inherits') + , EventEmitter = require('events').EventEmitter + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:polling'); +} + +function Polling(Receiver, receiveUrl, AjaxObject) { + debug(receiveUrl); + EventEmitter.call(this); + this.Receiver = Receiver; + this.receiveUrl = receiveUrl; + this.AjaxObject = AjaxObject; + this._scheduleReceiver(); +} + +inherits(Polling, EventEmitter); + +Polling.prototype._scheduleReceiver = function() { + debug('_scheduleReceiver'); + var self = this; + var poll = this.poll = new this.Receiver(this.receiveUrl, this.AjaxObject); + + poll.on('message', function(msg) { + debug('message', msg); + self.emit('message', msg); + }); + + poll.once('close', function(code, reason) { + debug('close', code, reason, self.pollIsClosing); + self.poll = poll = null; + + if (!self.pollIsClosing) { + if (reason === 'network') { + self._scheduleReceiver(); + } else { + self.emit('close', code || 1006, reason); + self.removeAllListeners(); + } + } + }); +}; + +Polling.prototype.abort = function() { + debug('abort'); + this.removeAllListeners(); + this.pollIsClosing = true; + if (this.poll) { + this.poll.abort(); + } +}; + +module.exports = Polling; + +}).call(this,require('_process')) + +},{"_process":110,"debug":1,"events":36,"inherits":7}],61:[function(require,module,exports){ +(function (process){ +'use strict'; + +var inherits = require('inherits') + , urlUtils = require('../../utils/url') + , BufferedSender = require('./buffered-sender') + , Polling = require('./polling') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:sender-receiver'); +} + +function SenderReceiver(transUrl, urlSuffix, senderFunc, Receiver, AjaxObject) { + var pollUrl = urlUtils.addPath(transUrl, urlSuffix); + debug(pollUrl); + var self = this; + BufferedSender.call(this, transUrl, senderFunc); + + this.poll = new Polling(Receiver, pollUrl, AjaxObject); + this.poll.on('message', function(msg) { + debug('poll message', msg); + self.emit('message', msg); + }); + this.poll.once('close', function(code, reason) { + debug('poll close', code, reason); + self.poll = null; + self.emit('close', code, reason); + self.close(); + }); +} + +inherits(SenderReceiver, BufferedSender); + +SenderReceiver.prototype.close = function() { + debug('close'); + this.removeAllListeners(); + if (this.poll) { + this.poll.abort(); + this.poll = null; + } + this.stop(); +}; + +module.exports = SenderReceiver; + +}).call(this,require('_process')) + +},{"../../utils/url":85,"./buffered-sender":58,"./polling":60,"_process":110,"debug":1,"inherits":7}],62:[function(require,module,exports){ +(function (process){ +'use strict'; + +var inherits = require('inherits') + , EventEmitter = require('events').EventEmitter + , EventSourceDriver = require('eventsource') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:receiver:eventsource'); +} + +function EventSourceReceiver(url) { + debug(url); + EventEmitter.call(this); + + var self = this; + var es = this.es = new EventSourceDriver(url); + es.onmessage = function(e) { + debug('message', e.data); + self.emit('message', decodeURI(e.data)); + }; + es.onerror = function(e) { + debug('error', es.readyState, e); + // ES on reconnection has readyState = 0 or 1. + // on network error it's CLOSED = 2 + var reason = (es.readyState !== 2 ? 'network' : 'permanent'); + self._cleanup(); + self._close(reason); + }; +} + +inherits(EventSourceReceiver, EventEmitter); + +EventSourceReceiver.prototype.abort = function() { + debug('abort'); + this._cleanup(); + this._close('user'); +}; + +EventSourceReceiver.prototype._cleanup = function() { + debug('cleanup'); + var es = this.es; + if (es) { + es.onmessage = es.onerror = null; + es.close(); + this.es = null; + } +}; + +EventSourceReceiver.prototype._close = function(reason) { + debug('close', reason); + var self = this; + // Safari and chrome < 15 crash if we close window before + // waiting for ES cleanup. See: + // https://code.google.com/p/chromium/issues/detail?id=89155 + setTimeout(function() { + self.emit('close', null, reason); + self.removeAllListeners(); + }, 200); +}; + +module.exports = EventSourceReceiver; + +}).call(this,require('_process')) + +},{"_process":110,"debug":1,"events":36,"eventsource":51,"inherits":7}],63:[function(require,module,exports){ +(function (process,global){ +'use strict'; + +var inherits = require('inherits') + , iframeUtils = require('../../utils/iframe') + , urlUtils = require('../../utils/url') + , EventEmitter = require('events').EventEmitter + , random = require('../../utils/random') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:receiver:htmlfile'); +} + +function HtmlfileReceiver(url) { + debug(url); + EventEmitter.call(this); + var self = this; + iframeUtils.polluteGlobalNamespace(); + + this.id = 'a' + random.string(6); + url = urlUtils.addQuery(url, 'c=' + decodeURIComponent(iframeUtils.WPrefix + '.' + this.id)); + + debug('using htmlfile', HtmlfileReceiver.htmlfileEnabled); + var constructFunc = HtmlfileReceiver.htmlfileEnabled ? + iframeUtils.createHtmlfile : iframeUtils.createIframe; + + global[iframeUtils.WPrefix][this.id] = { + start: function() { + debug('start'); + self.iframeObj.loaded(); + } + , message: function(data) { + debug('message', data); + self.emit('message', data); + } + , stop: function() { + debug('stop'); + self._cleanup(); + self._close('network'); + } + }; + this.iframeObj = constructFunc(url, function() { + debug('callback'); + self._cleanup(); + self._close('permanent'); + }); +} + +inherits(HtmlfileReceiver, EventEmitter); + +HtmlfileReceiver.prototype.abort = function() { + debug('abort'); + this._cleanup(); + this._close('user'); +}; + +HtmlfileReceiver.prototype._cleanup = function() { + debug('_cleanup'); + if (this.iframeObj) { + this.iframeObj.cleanup(); + this.iframeObj = null; + } + delete global[iframeUtils.WPrefix][this.id]; +}; + +HtmlfileReceiver.prototype._close = function(reason) { + debug('_close', reason); + this.emit('close', null, reason); + this.removeAllListeners(); +}; + +HtmlfileReceiver.htmlfileEnabled = false; + +// obfuscate to avoid firewalls +var axo = ['Active'].concat('Object').join('X'); +if (axo in global) { + try { + HtmlfileReceiver.htmlfileEnabled = !!new global[axo]('htmlfile'); + } catch (x) { + // intentionally empty + } +} + +HtmlfileReceiver.enabled = HtmlfileReceiver.htmlfileEnabled || iframeUtils.iframeEnabled; + +module.exports = HtmlfileReceiver; + +}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"../../utils/iframe":80,"../../utils/random":83,"../../utils/url":85,"_process":110,"debug":1,"events":36,"inherits":7}],64:[function(require,module,exports){ +(function (process,global){ +'use strict'; + +var utils = require('../../utils/iframe') + , random = require('../../utils/random') + , browser = require('../../utils/browser') + , urlUtils = require('../../utils/url') + , inherits = require('inherits') + , EventEmitter = require('events').EventEmitter + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:receiver:jsonp'); +} + +function JsonpReceiver(url) { + debug(url); + var self = this; + EventEmitter.call(this); + + utils.polluteGlobalNamespace(); + + this.id = 'a' + random.string(6); + var urlWithId = urlUtils.addQuery(url, 'c=' + encodeURIComponent(utils.WPrefix + '.' + this.id)); + + global[utils.WPrefix][this.id] = this._callback.bind(this); + this._createScript(urlWithId); + + // Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty. + this.timeoutId = setTimeout(function() { + debug('timeout'); + self._abort(new Error('JSONP script loaded abnormally (timeout)')); + }, JsonpReceiver.timeout); +} + +inherits(JsonpReceiver, EventEmitter); + +JsonpReceiver.prototype.abort = function() { + debug('abort'); + if (global[utils.WPrefix][this.id]) { + var err = new Error('JSONP user aborted read'); + err.code = 1000; + this._abort(err); + } +}; + +JsonpReceiver.timeout = 35000; +JsonpReceiver.scriptErrorTimeout = 1000; + +JsonpReceiver.prototype._callback = function(data) { + debug('_callback', data); + this._cleanup(); + + if (this.aborting) { + return; + } + + if (data) { + debug('message', data); + this.emit('message', data); + } + this.emit('close', null, 'network'); + this.removeAllListeners(); +}; + +JsonpReceiver.prototype._abort = function(err) { + debug('_abort', err); + this._cleanup(); + this.aborting = true; + this.emit('close', err.code, err.message); + this.removeAllListeners(); +}; + +JsonpReceiver.prototype._cleanup = function() { + debug('_cleanup'); + clearTimeout(this.timeoutId); + if (this.script2) { + this.script2.parentNode.removeChild(this.script2); + this.script2 = null; + } + if (this.script) { + var script = this.script; + // Unfortunately, you can't really abort script loading of + // the script. + script.parentNode.removeChild(script); + script.onreadystatechange = script.onerror = + script.onload = script.onclick = null; + this.script = null; + } + delete global[utils.WPrefix][this.id]; +}; + +JsonpReceiver.prototype._scriptError = function() { + debug('_scriptError'); + var self = this; + if (this.errorTimer) { + return; + } + + this.errorTimer = setTimeout(function() { + if (!self.loadedOkay) { + self._abort(new Error('JSONP script loaded abnormally (onerror)')); + } + }, JsonpReceiver.scriptErrorTimeout); +}; + +JsonpReceiver.prototype._createScript = function(url) { + debug('_createScript', url); + var self = this; + var script = this.script = global.document.createElement('script'); + var script2; // Opera synchronous load trick. + + script.id = 'a' + random.string(8); + script.src = url; + script.type = 'text/javascript'; + script.charset = 'UTF-8'; + script.onerror = this._scriptError.bind(this); + script.onload = function() { + debug('onload'); + self._abort(new Error('JSONP script loaded abnormally (onload)')); + }; + + // IE9 fires 'error' event after onreadystatechange or before, in random order. + // Use loadedOkay to determine if actually errored + script.onreadystatechange = function() { + debug('onreadystatechange', script.readyState); + if (/loaded|closed/.test(script.readyState)) { + if (script && script.htmlFor && script.onclick) { + self.loadedOkay = true; + try { + // In IE, actually execute the script. + script.onclick(); + } catch (x) { + // intentionally empty + } + } + if (script) { + self._abort(new Error('JSONP script loaded abnormally (onreadystatechange)')); + } + } + }; + // IE: event/htmlFor/onclick trick. + // One can't rely on proper order for onreadystatechange. In order to + // make sure, set a 'htmlFor' and 'event' properties, so that + // script code will be installed as 'onclick' handler for the + // script object. Later, onreadystatechange, manually execute this + // code. FF and Chrome doesn't work with 'event' and 'htmlFor' + // set. For reference see: + // http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html + // Also, read on that about script ordering: + // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order + if (typeof script.async === 'undefined' && global.document.attachEvent) { + // According to mozilla docs, in recent browsers script.async defaults + // to 'true', so we may use it to detect a good browser: + // https://developer.mozilla.org/en/HTML/Element/script + if (!browser.isOpera()) { + // Naively assume we're in IE + try { + script.htmlFor = script.id; + script.event = 'onclick'; + } catch (x) { + // intentionally empty + } + script.async = true; + } else { + // Opera, second sync script hack + script2 = this.script2 = global.document.createElement('script'); + script2.text = "try{var a = document.getElementById('" + script.id + "'); if(a)a.onerror();}catch(x){};"; + script.async = script2.async = false; + } + } + if (typeof script.async !== 'undefined') { + script.async = true; + } + + var head = global.document.getElementsByTagName('head')[0]; + head.insertBefore(script, head.firstChild); + if (script2) { + head.insertBefore(script2, head.firstChild); + } +}; + +module.exports = JsonpReceiver; + +}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"../../utils/browser":77,"../../utils/iframe":80,"../../utils/random":83,"../../utils/url":85,"_process":110,"debug":1,"events":36,"inherits":7}],65:[function(require,module,exports){ +(function (process){ +'use strict'; + +var inherits = require('inherits') + , EventEmitter = require('events').EventEmitter + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:receiver:xhr'); +} + +function XhrReceiver(url, AjaxObject) { + debug(url); + EventEmitter.call(this); + var self = this; + + this.bufferPosition = 0; + + this.xo = new AjaxObject('POST', url, null); + this.xo.on('chunk', this._chunkHandler.bind(this)); + this.xo.once('finish', function(status, text) { + debug('finish', status, text); + self._chunkHandler(status, text); + self.xo = null; + var reason = status === 200 ? 'network' : 'permanent'; + debug('close', reason); + self.emit('close', null, reason); + self._cleanup(); + }); +} + +inherits(XhrReceiver, EventEmitter); + +XhrReceiver.prototype._chunkHandler = function(status, text) { + debug('_chunkHandler', status); + if (status !== 200 || !text) { + return; + } + + for (var idx = -1; ; this.bufferPosition += idx + 1) { + var buf = text.slice(this.bufferPosition); + idx = buf.indexOf('\n'); + if (idx === -1) { + break; + } + var msg = buf.slice(0, idx); + if (msg) { + debug('message', msg); + this.emit('message', msg); + } + } +}; + +XhrReceiver.prototype._cleanup = function() { + debug('_cleanup'); + this.removeAllListeners(); +}; + +XhrReceiver.prototype.abort = function() { + debug('abort'); + if (this.xo) { + this.xo.close(); + debug('close'); + this.emit('close', null, 'user'); + this.xo = null; + } + this._cleanup(); +}; + +module.exports = XhrReceiver; + +}).call(this,require('_process')) + +},{"_process":110,"debug":1,"events":36,"inherits":7}],66:[function(require,module,exports){ +(function (process,global){ +'use strict'; + +var random = require('../../utils/random') + , urlUtils = require('../../utils/url') + ; + +var debug = function() {}; +if (process.env.NODE_ENV !== 'production') { + debug = require('debug')('sockjs-client:sender:jsonp'); +} + +var form, area; + +function createIframe(id) { + debug('createIframe', id); + try { + // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) + return global.document.createElement('