diff --git a/openvidu-insecure-angular/.angular-cli.json b/openvidu-insecure-angular/.angular-cli.json
new file mode 100644
index 00000000..bcfcd987
--- /dev/null
+++ b/openvidu-insecure-angular/.angular-cli.json
@@ -0,0 +1,44 @@
+{
+ "project": {
+ "version": "1.0.0-beta.17",
+ "name": "openvidu-insecure-angular"
+ },
+ "apps": [
+ {
+ "root": "src",
+ "outDir": "dist",
+ "assets": "assets",
+ "index": "index.html",
+ "main": "main.ts",
+ "test": "test.ts",
+ "tsconfig": "tsconfig.json",
+ "prefix": "app",
+ "mobile": false,
+ "styles": [
+ "styles.css"
+ ],
+ "scripts": [],
+ "environmentSource": "environments/environment.ts",
+ "environments": {
+ "dev": "environments/environment.ts",
+ "prod": "environments/environment.prod.ts"
+ }
+ }
+ ],
+ "addons": [],
+ "packages": [],
+ "e2e": {
+ "protractor": {
+ "config": "./protractor.conf.js"
+ }
+ },
+ "test": {
+ "karma": {
+ "config": "./karma.conf.js"
+ }
+ },
+ "defaults": {
+ "styleExt": "css",
+ "prefixInterfaces": false
+ }
+}
diff --git a/openvidu-insecure-angular/.gitignore b/openvidu-insecure-angular/.gitignore
new file mode 100644
index 00000000..938f477e
--- /dev/null
+++ b/openvidu-insecure-angular/.gitignore
@@ -0,0 +1,4 @@
+/node_modules
+.project
+.vscode
+yarn.lock
\ No newline at end of file
diff --git a/openvidu-insecure-angular/LICENSE b/openvidu-insecure-angular/LICENSE
new file mode 100644
index 00000000..8dada3ed
--- /dev/null
+++ b/openvidu-insecure-angular/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ 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.
diff --git a/openvidu-insecure-angular/README.md b/openvidu-insecure-angular/README.md
new file mode 100644
index 00000000..30c10dcc
--- /dev/null
+++ b/openvidu-insecure-angular/README.md
@@ -0,0 +1,88 @@
+# openvidu-insecure-angular
+
+This repository contains a group videoconference sample application implemented using OpenVidu. This application is a SPA page implemented in [Angular 2](http://angular.io) and was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.17.
+
+## Start OpenVidu Development Server
+
+To develop a videoconference application with OpenVidu you first have to start an OpenVidu Development Server, that contains all needed services. OpenVidu Development Server is distributed in a single docker image.
+
+To execute OpenVidu Development Server in your local development computer, you need to have docker software installed. You can [install it on Windows, Mac or Linux](https://docs.docker.com/engine/installation/).
+
+To start OpenVidu Development Server execute the following command (depending on your configuration it is is possible that you need to execute it with 'sudo'):
+
+
+
+And then wait to a log trace similar to this:
+
+
+INFO: Started OpenViduServer in 5.372 seconds (JVM running for 6.07)
+
+
+If you have installed Docker Toolbox in Windows or Mac, you need to know the IP address of your docker machine excuting the following command:
+
+
+docker-machine ip default
+
+
+Then, open in your browser and visit URL `https://127.0.0.1:8443` (or if you are using Docker Toolbox in Windows or Mac visit `https://:8443`). Then, browser will complain about insecure certificate. Please accept the selfsigned certificate as valid.
+
+Now you are ready to execute the sample application.
+
+## Executing sample application
+
+In this repository you have a sample JavaScript application that use OpenVidu Development Server to allow videoconferences between a group of users. Please clone it with the following command (you need git installed in your development machine):
+
+
+
+If you obtain an error executing this command, be sure you have installed Node 4 or highe together with NPM 3 or higher.
+
+Then, you execute the development Angular 2 server executing
+
+
+ng serve
+
+
+Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
+
+If you are using Docker Toolbox for Windows or Mac, you need to modify the sample application code. You have to change the following line in the file `src/app/app.component.ts`:
+
+
+this.openVidu = new OpenVidu("wss://127.0.0.1:8443/");
+
+
+You have to change `127.0.0.1` with the IP of the OpenVidu Development Server obtained in the previous step.
+
+Then you can go to `http://localhost:4200/` to use the sample application.
+
+As you can see, the user name and session is filled automatically in the form to make easier testing the app.
+
+If you open `http://localhost:4200/` in two tabs, you can simulate two users talking together. You can open as tabs as you want, but you need a very powerful development machine to test 3 or more users.
+
+For now, it is not possible use the sample application from a different computer.
+
+## Troubleshooting
+
+If you click the joing button and nothing happens, check the developer tools log. If you see
+
+
+Chrome: using SDP PlanB
+lang.js:234Angular 2 is running in the development mode. Call enableProdMode() to enable the production mode.
+Participant.js:32 New local participant undefined, streams opts: []
+jsonrpcclient.js:127 Connecting websocket to URI: wss://127.0.0.1:8443/room
+browser.js:38 WebSocket connection to 'wss://127.0.0.1:8443/room' failed: WebSocket opening handshake was canceledws @ browser.js:38WebSocketWithReconnection @ webSocketWithReconnection.js:59JsonRpcClient @ jsonrpcclient.js:125OpenVidu.initJsonRpcClient @ OpenVidu.js:63OpenVidu.connect @ OpenVidu.js:35AppComponent.joinSession @ app.component.ts:46_View_AppComponent1._handle_submit_5_0 @ AppComponent.ngfactory.js:533(anonymous function) @ view.js:403(anonymous function) @ dom_renderer.js:249(anonymous function) @ dom_events.js:26ZoneDelegate.invoke @ zone.js:232onInvoke @ ng_zone_impl.js:43ZoneDelegate.invoke @ zone.js:231Zone.runGuarded @ zone.js:128NgZoneImpl.runInnerGuarded @ ng_zone_impl.js:72NgZone.runGuarded @ ng_zone.js:235outsideHandler @ dom_events.js:26ZoneDelegate.invokeTask @ zone.js:265Zone.runTask @ zone.js:154ZoneTask.invoke @ zone.js:335
+
diff --git a/openvidu-insecure-angular/src/app/app.component.ts b/openvidu-insecure-angular/src/app/app.component.ts
new file mode 100644
index 00000000..d1592a80
--- /dev/null
+++ b/openvidu-insecure-angular/src/app/app.component.ts
@@ -0,0 +1,110 @@
+import { OpenVidu, Session, Subscriber, Publisher, Stream } from 'openvidu-browser';
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html'
+})
+export class AppComponent {
+
+ private OV: OpenVidu;
+ private session: Session;
+
+ toggle = false;
+ subscriber: Subscriber;
+ publisher: Publisher;
+ stream: Stream;
+
+ // Join form
+ sessionId: string;
+ token: string;
+
+ constructor() {
+ this.generateParticipantInfo();
+ }
+
+ private generateParticipantInfo() {
+ this.sessionId = 'SessionA';
+ this.token = 'Participant' + Math.floor(Math.random() * 100);
+ }
+
+ joinSession() {
+ this.sessionId = (document.getElementById('sessionId')).value;
+ this.token = (document.getElementById('token')).value;
+
+ this.OV = new OpenVidu('wss://' + location.hostname + ':8443/');
+ this.session = this.OV.initSession('apikey', this.sessionId);
+
+ // 2) Specify the actions when events take place
+ this.session.on('streamCreated', (event) => {
+ this.stream = event.stream;
+ this.subscriber = this.session.subscribe(event.stream, 'subscriber', {
+ insertMode: 'append',
+ width: '100%',
+ height: '100%'
+ });
+ this.subscriber.on('videoElementCreated', (e) => {
+ console.warn('VIDEO ELEMENT HAS BEEN CREATED BY SUBSCRIBER!');
+ console.warn(e);
+ });
+
+ });
+
+ this.session.on('streamDestroyed', (event) => {
+ console.warn('Stream has been destroyed!');
+ //event.preventDefault(); // Do not remove the HTML video element
+ });
+
+ // 3) Connect to the session
+ this.session.connect(this.token, (error) => {
+ // If the connection is successful, initialize a publisher and publish to the session
+ if (!error) {
+
+ // 4) Get your own camera stream with the desired resolution and publish it, only if the user is supposed to do so
+ this.publisher = this.OV.initPublisher('publisher', {
+ insertMode: 'append',
+ width: '100%',
+ height: '100%'
+ });
+
+ this.publisher.on('videoElementCreated', (event) => {
+ console.warn('VIDEO ELEMENT HAS BEEN CREATED BY PUBLISHER!');
+ console.warn(event);
+ });
+
+ // 5) Publish your stream
+ this.session.publish(this.publisher);
+
+ } else {
+ console.log('There was an error connecting to the session:', error.code, error.message);
+ }
+ });
+
+ return false;
+ }
+
+ leaveSession() {
+ if (this.OV) { this.session.disconnect(); };
+ this.session = null;
+ this.OV = null;
+ this.generateParticipantInfo();
+ }
+
+ testingAction() {
+ // UNSUBSCRIBE-SUBSCRIBE
+ /*if (!this.toggle) {
+ this.session.unsubscribe(this.subscriber);
+ } else {
+ this.subscriber = this.session.subscribe(this.stream, 'subscriber');
+ this.subscriber.on('videoElementCreated', (e) => {
+ console.warn('VIDEO ELEMENT HAS BEEN CREATED BY SUBSCRIBER!');
+ console.warn(e);
+ });
+ }
+ this.toggle = !this.toggle;*/
+
+ // PUBLISHER.DESTROY
+ /*this.publisher.destroy();*/
+ }
+
+}
diff --git a/openvidu-insecure-angular/src/app/app.module.ts b/openvidu-insecure-angular/src/app/app.module.ts
new file mode 100644
index 00000000..9d91ef7c
--- /dev/null
+++ b/openvidu-insecure-angular/src/app/app.module.ts
@@ -0,0 +1,21 @@
+import { BrowserModule } from '@angular/platform-browser';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { HttpModule } from '@angular/http';
+
+import { AppComponent } from './app.component';
+import { StreamComponent } from "./stream.component";
+
+@NgModule({
+ declarations: [
+ AppComponent, StreamComponent
+ ],
+ imports: [
+ BrowserModule,
+ FormsModule,
+ HttpModule
+ ],
+ providers: [],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
diff --git a/openvidu-insecure-angular/src/app/index.ts b/openvidu-insecure-angular/src/app/index.ts
new file mode 100644
index 00000000..875bdb2f
--- /dev/null
+++ b/openvidu-insecure-angular/src/app/index.ts
@@ -0,0 +1,2 @@
+export * from './app.component';
+export * from './app.module';
diff --git a/openvidu-insecure-angular/src/app/stream.component.ts b/openvidu-insecure-angular/src/app/stream.component.ts
new file mode 100644
index 00000000..9ebef09f
--- /dev/null
+++ b/openvidu-insecure-angular/src/app/stream.component.ts
@@ -0,0 +1,49 @@
+import { Component, Input } from '@angular/core';
+import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
+
+import { Stream } from 'openvidu-browser';
+
+@Component({
+ selector: 'stream',
+ styles: [`
+ .participant {
+ float: left;
+ width: 20%;
+ margin: 10px;
+ }
+ .participant video {
+ width: 100%;
+ height: auto;
+ }`],
+ template: `
+
+
{{stream.getId()}}
+
+
`
+})
+export class StreamComponent {
+
+ @Input()
+ stream: Stream;
+
+ videoSrc: SafeUrl;
+
+ constructor(private sanitizer: DomSanitizer) { }
+
+ ngOnInit() {
+
+ let int = setInterval(() => {
+ if (this.stream.getWrStream()) {
+ this.videoSrc = this.sanitizer.bypassSecurityTrustUrl(
+ URL.createObjectURL(this.stream.getWrStream()));
+ console.log("Video tag src=" + this.videoSrc);
+ clearInterval(int);
+ }
+ }, 1000);
+
+ //this.stream.addEventListener('src-added', () => {
+ // this.video.src = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(this.stream.getWrStream())).toString();
+ //});
+ }
+
+}
\ No newline at end of file
diff --git a/openvidu-insecure-angular/src/assets/.gitkeep b/openvidu-insecure-angular/src/assets/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/openvidu-insecure-angular/src/assets/.npmignore b/openvidu-insecure-angular/src/assets/.npmignore
new file mode 100644
index 00000000..e69de29b
diff --git a/openvidu-insecure-angular/src/environments/environment.prod.ts b/openvidu-insecure-angular/src/environments/environment.prod.ts
new file mode 100644
index 00000000..3612073b
--- /dev/null
+++ b/openvidu-insecure-angular/src/environments/environment.prod.ts
@@ -0,0 +1,3 @@
+export const environment = {
+ production: true
+};
diff --git a/openvidu-insecure-angular/src/environments/environment.ts b/openvidu-insecure-angular/src/environments/environment.ts
new file mode 100644
index 00000000..00313f16
--- /dev/null
+++ b/openvidu-insecure-angular/src/environments/environment.ts
@@ -0,0 +1,8 @@
+// The file contents for the current environment will overwrite these during build.
+// The build system defaults to the dev environment which uses `environment.ts`, but if you do
+// `ng build --env=prod` then `environment.prod.ts` will be used instead.
+// The list of which env maps to which file can be found in `angular-cli.json`.
+
+export const environment = {
+ production: false
+};
diff --git a/openvidu-insecure-angular/src/favicon.ico b/openvidu-insecure-angular/src/favicon.ico
new file mode 100644
index 00000000..8081c7ce
Binary files /dev/null and b/openvidu-insecure-angular/src/favicon.ico differ
diff --git a/openvidu-insecure-angular/src/index.html b/openvidu-insecure-angular/src/index.html
new file mode 100644
index 00000000..08101d16
--- /dev/null
+++ b/openvidu-insecure-angular/src/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ OpenviduNg2Example
+
+
+
+
+
+
+ Loading...
+
+
diff --git a/openvidu-insecure-angular/src/main.ts b/openvidu-insecure-angular/src/main.ts
new file mode 100644
index 00000000..5c3c5204
--- /dev/null
+++ b/openvidu-insecure-angular/src/main.ts
@@ -0,0 +1,12 @@
+import './polyfills.ts';
+
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { enableProdMode } from '@angular/core';
+import { environment } from './environments/environment';
+import { AppModule } from './app/';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule);
diff --git a/openvidu-insecure-angular/src/polyfills.ts b/openvidu-insecure-angular/src/polyfills.ts
new file mode 100644
index 00000000..3b4c55b0
--- /dev/null
+++ b/openvidu-insecure-angular/src/polyfills.ts
@@ -0,0 +1,19 @@
+// This file includes polyfills needed by Angular 2 and is loaded before
+// the app. You can add your own extra polyfills to this file.
+import 'core-js/es6/symbol';
+import 'core-js/es6/object';
+import 'core-js/es6/function';
+import 'core-js/es6/parse-int';
+import 'core-js/es6/parse-float';
+import 'core-js/es6/number';
+import 'core-js/es6/math';
+import 'core-js/es6/string';
+import 'core-js/es6/date';
+import 'core-js/es6/array';
+import 'core-js/es6/regexp';
+import 'core-js/es6/map';
+import 'core-js/es6/set';
+import 'core-js/es6/reflect';
+
+import 'core-js/es7/reflect';
+import 'zone.js/dist/zone';
diff --git a/openvidu-insecure-angular/src/styles.css b/openvidu-insecure-angular/src/styles.css
new file mode 100644
index 00000000..e50a47e7
--- /dev/null
+++ b/openvidu-insecure-angular/src/styles.css
@@ -0,0 +1 @@
+/* You can add global styles to this file, and also import other style files */
\ No newline at end of file
diff --git a/openvidu-insecure-angular/src/test.ts b/openvidu-insecure-angular/src/test.ts
new file mode 100644
index 00000000..7727c8e6
--- /dev/null
+++ b/openvidu-insecure-angular/src/test.ts
@@ -0,0 +1,34 @@
+import './polyfills.ts';
+
+import 'zone.js/dist/long-stack-trace-zone';
+import 'zone.js/dist/proxy.js';
+import 'zone.js/dist/sync-test';
+import 'zone.js/dist/jasmine-patch';
+import 'zone.js/dist/async-test';
+import 'zone.js/dist/fake-async-test';
+
+// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
+declare var __karma__: any;
+declare var require: any;
+
+// Prevent Karma from running prematurely.
+__karma__.loaded = function () {};
+
+
+Promise.all([
+ System.import('@angular/core/testing'),
+ System.import('@angular/platform-browser-dynamic/testing')
+])
+ // First, initialize the Angular testing environment.
+ .then(([testing, testingBrowser]) => {
+ testing.getTestBed().initTestEnvironment(
+ testingBrowser.BrowserDynamicTestingModule,
+ testingBrowser.platformBrowserDynamicTesting()
+ );
+ })
+ // Then we find all the tests.
+ .then(() => require.context('./', true, /\.spec\.ts/))
+ // And load the modules.
+ .then(context => context.keys().map(context))
+ // Finally, start Karma to run the tests.
+ .then(__karma__.start, __karma__.error);
diff --git a/openvidu-insecure-angular/src/tsconfig.json b/openvidu-insecure-angular/src/tsconfig.json
new file mode 100644
index 00000000..9b4c84cf
--- /dev/null
+++ b/openvidu-insecure-angular/src/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "declaration": false,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "lib": ["es6", "dom"],
+ "mapRoot": "./",
+ "module": "es6",
+ "moduleResolution": "node",
+ "outDir": "../dist/out-tsc",
+ "sourceMap": true,
+ "target": "es5",
+ "typeRoots": [
+ "../node_modules/@types"
+ ]
+ }
+}
diff --git a/openvidu-insecure-angular/src/typings.d.ts b/openvidu-insecure-angular/src/typings.d.ts
new file mode 100644
index 00000000..a73f5867
--- /dev/null
+++ b/openvidu-insecure-angular/src/typings.d.ts
@@ -0,0 +1,5 @@
+// Typings reference file, see links for more information
+// https://github.com/typings/typings
+// https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html
+
+declare var System: any;
diff --git a/openvidu-insecure-angular/tslint.json b/openvidu-insecure-angular/tslint.json
new file mode 100644
index 00000000..29f24ee2
--- /dev/null
+++ b/openvidu-insecure-angular/tslint.json
@@ -0,0 +1,112 @@
+{
+ "rulesDirectory": [
+ "node_modules/codelyzer"
+ ],
+ "rules": {
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ ],
+ "curly": true,
+ "eofline": true,
+ "forin": true,
+ "indent": [
+ true,
+ "spaces"
+ ],
+ "label-position": true,
+ "label-undefined": true,
+ "max-line-length": [
+ true,
+ 140
+ ],
+ "member-access": false,
+ "member-ordering": [
+ true,
+ "static-before-instance",
+ "variables-before-functions"
+ ],
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-key": true,
+ "no-duplicate-variable": true,
+ "no-empty": false,
+ "no-eval": true,
+ "no-inferrable-types": true,
+ "no-shadowed-variable": true,
+ "no-string-literal": false,
+ "no-switch-case-fall-through": true,
+ "no-trailing-whitespace": true,
+ "no-unused-expression": true,
+ "no-unused-variable": true,
+ "no-unreachable": true,
+ "no-use-before-declare": true,
+ "no-var-keyword": true,
+ "object-literal-sort-keys": false,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "quotemark": [
+ true,
+ "single"
+ ],
+ "radix": true,
+ "semicolon": [
+ "always"
+ ],
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "typedef-whitespace": [
+ true,
+ {
+ "call-signature": "nospace",
+ "index-signature": "nospace",
+ "parameter": "nospace",
+ "property-declaration": "nospace",
+ "variable-declaration": "nospace"
+ }
+ ],
+ "variable-name": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator",
+ "check-type"
+ ],
+
+ "directive-selector-prefix": [true, "app"],
+ "component-selector-prefix": [true, "app"],
+ "directive-selector-name": [true, "camelCase"],
+ "component-selector-name": [true, "kebab-case"],
+ "directive-selector-type": [true, "attribute"],
+ "component-selector-type": [true, "element"],
+ "use-input-property-decorator": true,
+ "use-output-property-decorator": true,
+ "use-host-property-decorator": true,
+ "no-input-rename": true,
+ "no-output-rename": true,
+ "use-life-cycle-interface": true,
+ "use-pipe-transform-interface": true,
+ "component-class-suffix": true,
+ "directive-class-suffix": true
+ }
+}
diff --git a/openvidu-insecure-js/LICENSE b/openvidu-insecure-js/LICENSE
new file mode 100644
index 00000000..8dada3ed
--- /dev/null
+++ b/openvidu-insecure-js/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ 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.
diff --git a/openvidu-insecure-js/README.md b/openvidu-insecure-js/README.md
new file mode 100644
index 00000000..a0c8d511
--- /dev/null
+++ b/openvidu-insecure-js/README.md
@@ -0,0 +1,78 @@
+# openvidu-insecure-js
+
+This repository contains a group videoconference sample application implemented using OpenVidu. This application is a SPA page implemented in plain JavaScript (without any JavaScript framework).
+
+## Start OpenVidu Development Server
+
+To develop a videoconference application with OpenVidu you first have to start an OpenVidu Development Server, that contains all needed services. OpenVidu Development Server is distributed in a single docker image.
+
+To execute OpenVidu Development Server in your local development computer, you need to have docker software installed. You can [install it on Windows, Mac or Linux](https://docs.docker.com/engine/installation/).
+
+To start OpenVidu Development Server execute the following command (depending on your configuration it is is possible that you need to execute it with 'sudo'):
+
+
+
+And then wait to a log trace similar to this:
+
+
+INFO: Started OpenViduServer in 5.372 seconds (JVM running for 6.07)
+
+
+If you have installed Docker Toolbox in Windows or Mac, you need to know the IP address of your docker machine excuting the following command:
+
+
+docker-machine ip default
+
+
+Then, open in your browser and visit URL `https://127.0.0.1:8443` (or if you are using Docker Toolbox in Windows or Mac visit `https://:8443`). Then, browser will complain about insecure certificate. Please accept the selfsigned certificate as valid.
+
+Now you are ready to execute the sample application.
+
+## Executing sample application
+
+In this repository you have a sample JavaScript application that use OpenVidu Development Server to allow videoconferences between a group of users. Please clone it with the following command (you need git installed in your development machine):
+
+
+
+First, you need an http web server installed in your development computer to execute the sample application. If you have node.js installed in your development machine, you can use [http-server] to serve application files.(https://github.com/indexzero/http-server). It can be installed with:
+
+
+npm install http-server -g
+
+
+To execute the sample application, execute the following command in the project:
+
+
+
+If you are using Docker Toolbox for Windows or Mac, you need to modify the sample application code. You have to change the following line in the file `web/app.js`:
+
+
+openVidu = new OpenVidu("wss://127.0.0.1:8443/");
+
+
+You have to change `127.0.0.1` with the IP of the OpenVidu Development Server obtained in the previous step.
+
+Then you can go to `http://127.0.0.1:8080` to execute the sample application.
+
+As you can see, the user name and session is filled automatically in the form to make easier testing the app.
+
+If you open `http://127.0.0.1:8080` in two tabs, you can simulate two users talking together. You can open as tabs as you want, but you need a very powerful development machine to test 3 or more users.
+
+For now, it is not possible use the sample application from a different computer.
+
+## Sample application code
+
+This application is very simple. It has only 4 files:
+* `OpenVidu.js`: OpenVidu client. You don't have to manipulate this file.
+* `app.js`: Sample application main JavaScritp file. You can manipulate this file to adapt it to your necesities.
+* `index.html`: HTML file. It contains the HTML code for the form to connect to a videoconference and for the videoconference itself. You can manipulate this file to adapt it to your necesities.
+* `style.css`: Some CSS classes to style HTML. You can manipulate this file to adapt it to your necesities.
+
+
diff --git a/openvidu-insecure-js/web/OpenVidu.js b/openvidu-insecure-js/web/OpenVidu.js
new file mode 100644
index 00000000..92f63087
--- /dev/null
+++ b/openvidu-insecure-js/web/OpenVidu.js
@@ -0,0 +1,17843 @@
+(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() {
+ // NB: In an Electron preload script, document will be defined but not fully
+ // initialized. Since we know we're in Chrome, we'll just detect this case
+ // explicitly
+ if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') {
+ return true;
+ }
+
+ // is webkit? http://stackoverflow.com/a/16459606/376773
+ // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632
+ return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
+ // is firebug? http://stackoverflow.com/a/398120/376773
+ (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
+ // is firefox >= v31?
+ // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
+ (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) ||
+ // double check webkit in userAgent just in case we are in a worker
+ (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/));
+}
+
+/**
+ * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
+ */
+
+exports.formatters.j = function(v) {
+ try {
+ return JSON.stringify(v);
+ } catch (err) {
+ return '[UnexpectedJSONParseError]: ' + err.message;
+ }
+};
+
+
+/**
+ * Colorize log arguments if enabled.
+ *
+ * @api public
+ */
+
+function formatArgs(args) {
+ var useColors = this.useColors;
+
+ args[0] = (useColors ? '%c' : '')
+ + this.namespace
+ + (useColors ? ' %c' : ' ')
+ + args[0]
+ + (useColors ? '%c ' : ' ')
+ + '+' + exports.humanize(this.diff);
+
+ if (!useColors) return;
+
+ var c = 'color: ' + this.color;
+ args.splice(1, 0, c, 'color: inherit')
+
+ // 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-zA-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);
+}
+
+/**
+ * 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) {}
+
+ // If debug isn't set in LS, and we're in Electron, try to load $DEBUG
+ if (!r && typeof process !== 'undefined' && 'env' in process) {
+ r = process.env.DEBUG;
+ }
+
+ 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) {}
+}
+
+}).call(this,require('_process'))
+
+},{"./debug":2,"_process":115}],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 = createDebug.debug = createDebug['default'] = createDebug;
+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, lower or upper-case letter, i.e. "n" and "N".
+ */
+
+exports.formatters = {};
+
+/**
+ * Previous log timestamp.
+ */
+
+var prevTime;
+
+/**
+ * Select a color.
+ * @param {String} namespace
+ * @return {Number}
+ * @api private
+ */
+
+function selectColor(namespace) {
+ var hash = 0, i;
+
+ for (i in namespace) {
+ hash = ((hash << 5) - hash) + namespace.charCodeAt(i);
+ hash |= 0; // Convert to 32bit integer
+ }
+
+ return exports.colors[Math.abs(hash) % exports.colors.length];
+}
+
+/**
+ * Create a debugger with the given `namespace`.
+ *
+ * @param {String} namespace
+ * @return {Function}
+ * @api public
+ */
+
+function createDebug(namespace) {
+
+ function debug() {
+ // disabled?
+ if (!debug.enabled) return;
+
+ var self = debug;
+
+ // set `diff` timestamp
+ var curr = +new Date();
+ var ms = curr - (prevTime || curr);
+ self.diff = ms;
+ self.prev = prevTime;
+ self.curr = curr;
+ prevTime = curr;
+
+ // turn the `arguments` into a proper Array
+ var args = new Array(arguments.length);
+ for (var i = 0; i < args.length; i++) {
+ args[i] = arguments[i];
+ }
+
+ args[0] = exports.coerce(args[0]);
+
+ if ('string' !== typeof args[0]) {
+ // anything else let's inspect with %O
+ args.unshift('%O');
+ }
+
+ // apply any `formatters` transformations
+ var index = 0;
+ args[0] = args[0].replace(/%([a-zA-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;
+ });
+
+ // apply env-specific formatting (colors, etc.)
+ exports.formatArgs.call(self, args);
+
+ var logFn = debug.log || exports.log || console.log.bind(console);
+ logFn.apply(self, args);
+ }
+
+ debug.namespace = namespace;
+ debug.enabled = exports.enabled(namespace);
+ debug.useColors = exports.useColors();
+ debug.color = selectColor(namespace);
+
+ // env-specific initialization logic for debug instances
+ if ('function' === typeof exports.init) {
+ exports.init(debug);
+ }
+
+ return debug;
+}
+
+/**
+ * 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);
+
+ exports.names = [];
+ exports.skips = [];
+
+ var split = (typeof namespaces === 'string' ? 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":102}],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":33,"ws":104}],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":114,"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')
+var logger = window.Logger || console
+
+// var gUM = navigator.mediaDevices.getUserMedia || function (constraints) {
+// return new Promise(navigator.getUserMedia(constraints, function (stream) {
+// videoStream = stream
+// start()
+// }).eror(callback));
+// }
+
+try {
+ require('kurento-browser-extensions')
+} catch (error) {
+ if (typeof getScreenConstraints === 'undefined') {
+ logger.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') {
+ logger.info(browser.name + ": using SDP PlanB")
+ usePlanB = true
+}
+
+function noop(error) {
+ if (error) logger.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) {
+ logger.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.ontrack = 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)
+ }
+
+ logger.debug('Remote ICE candidate received', iceCandidate)
+ 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 = {
+ offerToReceiveAudio: (mode !== 'sendonly' && offerAudio),
+ offerToReceiveVideo: (mode !== 'sendonly' && offerVideo)
+ }
+
+ //FIXME: clarify possible constraints passed to createOffer()
+ /*var constraints = recursive(browserDependantConstraints,
+ connectionConstraints)*/
+
+ var constraints = browserDependantConstraints;
+
+ logger.info('constraints: ' + JSON.stringify(constraints))
+
+ pc.createOffer(constraints).then(function (offer) {
+ logger.info('Created SDP offer')
+ offer = mangleSdpToAddSimulcast(offer)
+ return pc.setLocalDescription(offer)
+ }).then(function () {
+ var localDescription = pc.localDescription
+ logger.info('Local description set', localDescription.sdp)
+ if (multistream && usePlanB) {
+ localDescription = interop.toUnifiedPlan(localDescription)
+ logger.info('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()
+
+ logger.info('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 {
+ logger.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)
+ logger.info('asnwer::planB', dumpSDP(planBAnswer))
+ answer = planBAnswer
+ }
+
+ logger.info('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)
+ logger.info('offer::planB', dumpSDP(planBOffer))
+ offer = planBOffer
+ }
+
+ logger.info('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)
+ logger.info('Created SDP answer')
+ return pc.setLocalDescription(answer)
+ }).then(function () {
+ var localDescription = pc.localDescription
+ if (multistream && usePlanB) {
+ localDescription = interop.toUnifiedPlan(localDescription)
+ logger.info('answer::origPlanB->UnifiedPlan', dumpSDP(
+ localDescription))
+ }
+ logger.info('Local description set', localDescription.sdp)
+ callback(null, localDescription.sdp)
+ }).catch(callback)
+ }
+
+ function mangleSdpToAddSimulcast(answer) {
+ if (simulcast) {
+ if (browser.name === 'Chrome' || browser.name === 'Chromium') {
+ logger.info('Adding multicast info')
+ answer = new RTCSessionDescription({
+ 'type': answer.type,
+ 'sdp': removeFIDFromOffer(answer.sdp) + getSimulcastInfo(
+ videoStream)
+ })
+ } else {
+ logger.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
+ }
+
+ navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
+ videoStream = stream
+ start()
+ }).catch(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 () {
+ logger.info('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) {
+ logger.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":114,"freeice":3,"hark":6,"inherits":7,"kurento-browser-extensions":undefined,"merge":20,"sdp-translator":29,"ua-parser-js":86,"uuid":90}],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 0) {
+ return parse(val);
+ } else if (type === 'number' && isNaN(val) === false) {
+ return options.long ? fmtLong(val) : fmtShort(val);
+ }
+ throw new Error(
+ 'val is not a non-empty string or a valid number. val=' +
+ JSON.stringify(val)
+ );
+};
+
+/**
+ * Parse the given `str` and return milliseconds.
+ *
+ * @param {String} str
+ * @return {Number}
+ * @api private
+ */
+
+function parse(str) {
+ str = String(str);
+ if (str.length > 100) {
+ 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;
+ default:
+ return undefined;
+ }
+}
+
+/**
+ * Short format for `ms`.
+ *
+ * @param {Number} ms
+ * @return {String}
+ * @api private
+ */
+
+function fmtShort(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 fmtLong(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';
+
+/**
+ * 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;
+};
+
+},{}],24:[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";
+ }
+ });
+});
+
+},{}],25:[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":26,"./writer":27}],26:[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":24}],27:[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":24}],28:[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;
+};
+
+
+},{}],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.
+ */
+
+exports.Interop = require('./interop');
+
+},{"./interop":30}],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.
+ */
+
+/* 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":28,"./transform":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.
+ */
+
+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":25}],32:[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: // extension handling, in particular ufrag
+ candidate[parts[i]] = parts[i + 1];
+ 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 ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+SDPUtils.parseIceOptions = function(line) {
+ return line.substr(14).split(' ');
+}
+
+// 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
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+ var parts = line.substr(9).split(' ');
+ return {
+ id: parseInt(parts[0], 10),
+ direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
+ uri: parts[1]
+ };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+ return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+ (headerExtension.direction && headerExtension.direction !== 'sendrecv'
+ ? '/' + headerExtension.direction
+ : '') +
+ ' ' + 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 the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+SDPUtils.getMid = function(mediaSection) {
+ var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
+ if (mid) {
+ return mid.substr(6);
+ }
+}
+
+SDPUtils.parseFingerprint = function(line) {
+ var parts = line.substr(14).split(' ');
+ return {
+ algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+ value: parts[1]
+ };
+};
+
+// 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.matchPrefix(mediaSection + sessionpart,
+ 'a=fingerprint:');
+ // Note: a=setup line is ignored since we use the 'auto' role.
+ // Note2: 'algorithm' is not case sensitive except in Edge.
+ return {
+ role: 'auto',
+ fingerprints: lines.map(SDPUtils.parseFingerprint)
+ };
+};
+
+// 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);
+ });
+ var maxptime = 0;
+ caps.codecs.forEach(function(codec) {
+ if (codec.maxptime > maxptime) {
+ maxptime = codec.maxptime;
+ }
+ });
+ if (maxptime > 0) {
+ sdp += 'a=maxptime:' + maxptime + '\r\n';
+ }
+ sdp += 'a=rtcp-mux\r\n';
+
+ caps.headerExtensions.forEach(function(extension) {
+ sdp += SDPUtils.writeExtmap(extension);
+ });
+ // FIXME: write fecMechanisms.
+ 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: {
+ 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;
+};
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+SDPUtils.parseRtcpParameters = function(mediaSection) {
+ var rtcpParameters = {};
+
+ var cname;
+ // Gets the first SSRC. Note that with RTX there might be multiple
+ // SSRCs.
+ var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(obj) {
+ return obj.attribute === 'cname';
+ })[0];
+ if (remoteSsrc) {
+ rtcpParameters.cname = remoteSsrc.value;
+ rtcpParameters.ssrc = remoteSsrc.ssrc;
+ }
+
+ // Edge uses the compound attribute instead of reducedSize
+ // compound is !reducedSize
+ var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
+ rtcpParameters.reducedSize = rsize.length > 0;
+ rtcpParameters.compound = rsize.length === 0;
+
+ // parses the rtcp-mux attrÑ–bute.
+ // Note that Edge does not support unmuxed RTCP.
+ var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
+ rtcpParameters.mux = mux.length > 0;
+
+ return rtcpParameters;
+};
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+SDPUtils.parseMsid = function(mediaSection) {
+ var parts;
+ var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
+ if (spec.length === 1) {
+ parts = spec[0].substr(7).split(' ');
+ return {stream: parts[0], track: parts[1]};
+ }
+ var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(parts) {
+ return parts.attribute === 'msid';
+ });
+ if (planB.length > 0) {
+ parts = planB[0].value.split(' ');
+ return {stream: parts[0], track: parts[1]};
+ }
+};
+
+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.direction) {
+ sdp += 'a=' + transceiver.direction + '\r\n';
+ } else 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';
+ }
+
+ if (transceiver.rtpSender) {
+ // spec.
+ var msid = 'msid:' + stream.id + ' ' +
+ transceiver.rtpSender.track.id + '\r\n';
+ sdp += 'a=' + msid;
+
+ // for Chrome.
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+ ' ' + msid;
+ if (transceiver.sendEncodingParameters[0].rtx) {
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+ ' ' + msid;
+ sdp += 'a=ssrc-group:FID ' +
+ transceiver.sendEncodingParameters[0].ssrc + ' ' +
+ transceiver.sendEncodingParameters[0].rtx.ssrc +
+ '\r\n';
+ }
+ }
+ // FIXME: this should be written by writeRtpDescription.
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+ ' cname:' + SDPUtils.localCName + '\r\n';
+ if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.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';
+};
+
+SDPUtils.getKind = function(mediaSection) {
+ var lines = SDPUtils.splitLines(mediaSection);
+ var mline = lines[0].split(' ');
+ return mline[0].substr(2);
+};
+
+SDPUtils.isRejected = function(mediaSection) {
+ return mediaSection.split(' ', 2)[1] === '0';
+};
+
+// Expose public methods.
+module.exports = SDPUtils;
+
+},{}],33:[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":46,"./transport-list":48}],34:[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":36,"inherits":7}],35:[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":37,"inherits":7}],36:[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;
+
+},{}],37:[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;
+
+},{}],38:[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":36,"inherits":7}],39:[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":79,"json3":8}],40:[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":39,"./info-iframe-receiver":42,"./location":45,"./utils/event":78,"./utils/iframe":79,"./utils/url":84,"_process":115,"debug":1,"json3":8}],41:[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":81,"_process":115,"debug":1,"events":35,"inherits":7,"json3":8}],42:[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":41,"./transport/sender/xhr-local":69,"events":35,"inherits":7,"json3":8}],43:[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":42,"./transport/iframe":54,"./utils/event":78,"_process":115,"debug":1,"events":35,"inherits":7,"json3":8}],44:[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":41,"./info-iframe":43,"./transport/sender/xdr":66,"./transport/sender/xhr-cors":67,"./transport/sender/xhr-fake":68,"./transport/sender/xhr-local":69,"./utils/url":84,"_process":115,"debug":1,"events":35,"inherits":7}],45:[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 : {})
+
+},{}],46:[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":34,"./event/event":36,"./event/eventtarget":37,"./event/trans-message":38,"./iframe-bootstrap":40,"./info-receiver":44,"./location":45,"./shims":47,"./utils/browser":76,"./utils/escape":77,"./utils/event":78,"./utils/log":80,"./utils/object":81,"./utils/random":82,"./utils/transport":83,"./utils/url":84,"./version":85,"_process":115,"debug":1,"inherits":7,"json3":8,"url-parse":87}],47:[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);
+
+},{}],48:[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":52,"./transport/htmlfile":53,"./transport/jsonp-polling":55,"./transport/lib/iframe-wrap":58,"./transport/websocket":70,"./transport/xdr-polling":71,"./transport/xdr-streaming":72,"./transport/xhr-polling":73,"./transport/xhr-streaming":74}],49:[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":78,"../../utils/url":84,"_process":115,"debug":1,"events":35,"inherits":7}],50:[function(require,module,exports){
+(function (global){
+module.exports = global.EventSource;
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{}],51:[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 : {})
+
+},{}],52:[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":56,"./receiver/eventsource":61,"./sender/xhr-cors":67,"eventsource":50,"inherits":7}],53:[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":56,"./receiver/htmlfile":62,"./sender/xhr-local":69,"inherits":7}],54:[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":78,"../utils/iframe":79,"../utils/random":82,"../utils/url":84,"../version":85,"_process":115,"debug":1,"events":35,"inherits":7,"json3":8}],55:[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":60,"./receiver/jsonp":63,"./sender/jsonp":65,"inherits":7}],56:[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":84,"./sender-receiver":60,"_process":115,"debug":1,"inherits":7}],57:[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":115,"debug":1,"events":35,"inherits":7}],58:[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":81,"../iframe":54,"inherits":7}],59:[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":115,"debug":1,"events":35,"inherits":7}],60:[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":84,"./buffered-sender":57,"./polling":59,"_process":115,"debug":1,"inherits":7}],61:[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":115,"debug":1,"events":35,"eventsource":50,"inherits":7}],62:[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":79,"../../utils/random":82,"../../utils/url":84,"_process":115,"debug":1,"events":35,"inherits":7}],63:[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":76,"../../utils/iframe":79,"../../utils/random":82,"../../utils/url":84,"_process":115,"debug":1,"events":35,"inherits":7}],64:[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":115,"debug":1,"events":35,"inherits":7}],65:[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('