├── .idea
├── .name
├── copyright
│ └── profiles_settings.xml
├── scopes
│ └── scope_settings.xml
├── encodings.xml
├── vcs.xml
├── modules.xml
├── jsLibraryMappings.xml
├── runConfigurations
│ └── bin_www.xml
├── codeStyleSettings.xml
├── libraries
│ └── AppRTC_node_server_node_modules.xml
├── compiler.xml
├── misc.xml
└── uiDesigner.xml
├── public
├── html
│ ├── google1b7eb21c5b594ba0.html
│ ├── error.jade
│ ├── layout.jade
│ ├── index_template.json
│ ├── manifest.json
│ ├── help.html
│ ├── full_template.jade
│ ├── params.html
│ └── index_template.jade
├── images
│ ├── apprtc-16.png
│ ├── apprtc-22.png
│ ├── apprtc-32.png
│ ├── apprtc-48.png
│ ├── apprtc-128.png
│ └── webrtc-icon-192x192.png
├── js
│ ├── README.md
│ ├── windowport.js
│ ├── infobox_test.js
│ ├── constants.js
│ ├── storage.js
│ ├── appcontroller_test.js
│ ├── loopback.js
│ ├── appwindow.js
│ ├── remotewebsocket.js
│ ├── test_mocks.js
│ ├── stats.js
│ ├── utils_test.js
│ ├── sdputils_test.js
│ ├── testpolyfills.js
│ ├── signalingchannel.js
│ ├── remotewebsocket_test.js
│ ├── background.js
│ ├── call_test.js
│ ├── signalingchannel_test.js
│ ├── util.js
│ ├── roomselection.js
│ ├── background_test.js
│ ├── adapter.js
│ ├── infobox.js
│ ├── peerconnectionclient_test.js
│ ├── roomselection_test.js
│ ├── sdputils.js
│ └── peerconnectionclient.js
└── css
│ └── main.css
├── .gitignore
├── routes
└── users.js
├── package.json
├── AppRTC-node-server.iml
├── README.md
├── app.js
├── bin
└── www
└── lib
└── rooms.js
/.idea/.name:
--------------------------------------------------------------------------------
1 | AppRTC-node-server
--------------------------------------------------------------------------------
/public/html/google1b7eb21c5b594ba0.html:
--------------------------------------------------------------------------------
1 | google-site-verification: google1b7eb21c5b594ba0.html
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.sw*
3 | *~
4 | /build/
5 | node_modules/
6 | webroot/
7 | /workspace/
8 |
--------------------------------------------------------------------------------
/public/images/apprtc-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ISBX/apprtc-node-server/HEAD/public/images/apprtc-16.png
--------------------------------------------------------------------------------
/public/images/apprtc-22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ISBX/apprtc-node-server/HEAD/public/images/apprtc-22.png
--------------------------------------------------------------------------------
/public/images/apprtc-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ISBX/apprtc-node-server/HEAD/public/images/apprtc-32.png
--------------------------------------------------------------------------------
/public/images/apprtc-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ISBX/apprtc-node-server/HEAD/public/images/apprtc-48.png
--------------------------------------------------------------------------------
/public/images/apprtc-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ISBX/apprtc-node-server/HEAD/public/images/apprtc-128.png
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/html/error.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/public/images/webrtc-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ISBX/apprtc-node-server/HEAD/public/images/webrtc-icon-192x192.png
--------------------------------------------------------------------------------
/public/html/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 | link(rel='stylesheet', href='/stylesheets/style.css')
6 | body
7 | block content
--------------------------------------------------------------------------------
/.idea/scopes/scope_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/html/index_template.json:
--------------------------------------------------------------------------------
1 | {
2 | "chromeapp": true,
3 | "description": "This data is used to build the Chrome App. index.html is transformed to appwindow.html using jinja2 and the data from this file."
4 | }
5 |
--------------------------------------------------------------------------------
/routes/users.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | /* GET users listing. */
5 | router.get('/', function(req, res, next) {
6 | res.send('respond with a resource');
7 | });
8 |
9 | module.exports = router;
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/html/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "AppRTC",
3 | "name": "WebRTC reference app",
4 | "icons": [
5 | {
6 | "src": "/images/webrtc-icon-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png",
9 | "density": "4.0"
10 | }
11 | ],
12 | "start_url": "/",
13 | "display": "standalone",
14 | "orientation": "portrait"
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AppRTC-node-server",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./bin/www"
7 | },
8 | "dependencies": {
9 | "body-parser": "~1.12.0",
10 | "cookie-parser": "~1.3.4",
11 | "debug": "~2.1.1",
12 | "express": "~4.12.0",
13 | "jade": "~1.9.2",
14 | "morgan": "~1.5.1",
15 | "serve-favicon": "~2.2.0",
16 | "stylus": "0.42.3"
17 | }
18 | }
--------------------------------------------------------------------------------
/.idea/runConfigurations/bin_www.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/codeStyleSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/AppRTC-node-server.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/libraries/AppRTC_node_server_node_modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/public/html/help.html:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
13 | WebRtc Demo App Help
14 |
15 |
16 | TODO
17 |
18 |
19 |
--------------------------------------------------------------------------------
/public/js/README.md:
--------------------------------------------------------------------------------
1 | # Javascript object hierarchy #
2 |
3 | AppController: The controller that connects the UI and the model "Call". It owns
4 | Call, InfoBox and RoomSelection.
5 |
6 | Call: Manages everything needed to make a call. It owns SignalingChannel and
7 | PeerConnectionClient.
8 |
9 | SignalingChannel: Wrapper of the WebSocket connection.
10 |
11 | PeerConnectionClient: Wrapper of RTCPeerConnection.
12 |
13 | InfoBox: Wrapper of the info div utilities.
14 |
15 | RoomSelection: Wrapper for the room selection UI. It owns Storage.
16 |
17 | Storage: Wrapper for localStorage/Chrome app storage API.
18 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AppRTC - NodeJS implementation of the Google WebRTC Demo
2 |
3 | ## About
4 | AppRTC-node-server is a straight port of the AppRTC Python Server from the Google WebRTC Demo to run entirely in the NodeJS environment.
5 |
6 | ## Notes
7 | This still a work in progress. We are in the process of cleaning up the code and making several enhancements:
8 |
9 | 1. Implementing as a node module so you can easily adapt to your project
10 | 2. Refactoring the code to optimize for NodeJS
11 | 3. Implementing options for memcache or redis cluster for scaling video chat sessions
12 | 4. Providing more documentation and extensibility
13 | 5. Adding a built-in Turn Server for better WebRTC portability
14 |
15 |
16 | ## Setup
17 | Setting up the environment just requires the following:
18 |
19 | ```
20 | git clone https://github.com/ISBX/apprtc-node-server.git ./apprtc-node-server
21 | cd ./apprtc-node-server
22 | npm install
23 | ```
24 |
25 | ## Running the AppRTC Node Server
26 | The apprtc-node-server uses ExpressJS. To run the node server after setup just execute:
27 |
28 | ```
29 | node ./bin/www
30 | ```
31 |
32 | Navigate to `http://localhost:3000` to run the WebRTC Demo
--------------------------------------------------------------------------------
/public/js/windowport.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals trace, chrome */
12 | /* exported apprtc, apprtc.windowPort */
13 |
14 | 'use strict';
15 |
16 | // This is used to communicate from the Chrome App window to background.js.
17 | // It opens a Port object to send and receive messages. When the Chrome
18 | // App window is closed, background.js receives notification and can
19 | // handle clean up tasks.
20 | var apprtc = apprtc || {};
21 | apprtc.windowPort = apprtc.windowPort || {};
22 | (function() {
23 | var port_;
24 |
25 | apprtc.windowPort.sendMessage = function(message) {
26 | var port = getPort_();
27 | try {
28 | port.postMessage(message);
29 | }
30 | catch (ex) {
31 | trace('Error sending message via port: ' + ex);
32 | }
33 | };
34 |
35 | apprtc.windowPort.addMessageListener = function(listener) {
36 | var port = getPort_();
37 | port.onMessage.addListener(listener);
38 | };
39 |
40 | var getPort_ = function() {
41 | if (!port_) {
42 | port_ = chrome.runtime.connect();
43 | }
44 | return port_;
45 | };
46 | })();
47 |
--------------------------------------------------------------------------------
/public/js/infobox_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals TestCase, assertEquals, InfoBox */
12 |
13 | 'use strict';
14 |
15 | var InfoBoxTest = new TestCase('InfoBoxTest');
16 |
17 | InfoBoxTest.prototype.testFormatBitrate = function() {
18 | assertEquals('Format bps.', '789 bps', InfoBox.formatBitrate_(789));
19 | assertEquals('Format kbps.', '78.9 kbps', InfoBox.formatBitrate_(78912));
20 | assertEquals('Format Mbps.', '7.89 Mbps', InfoBox.formatBitrate_(7891234));
21 | };
22 |
23 | InfoBoxTest.prototype.testFormatInterval = function() {
24 | assertEquals('Format 00:01', '00:01', InfoBox.formatInterval_(1999));
25 | assertEquals('Format 00:12', '00:12', InfoBox.formatInterval_(12500));
26 | assertEquals('Format 01:23', '01:23', InfoBox.formatInterval_(83123));
27 | assertEquals('Format 12:34', '12:34', InfoBox.formatInterval_(754000));
28 | assertEquals('Format 01:23:45', '01:23:45',
29 | InfoBox.formatInterval_(5025000));
30 | assertEquals('Format 12:34:56', '12:34:56',
31 | InfoBox.formatInterval_(45296000));
32 | assertEquals('Format 123:45:43', '123:45:43',
33 | InfoBox.formatInterval_(445543000));
34 | };
35 |
--------------------------------------------------------------------------------
/public/js/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* exported Constants */
12 | 'use strict';
13 |
14 | var Constants = {
15 | // Action type for remote web socket communication.
16 | WS_ACTION: 'ws',
17 | // Action type for remote xhr communication.
18 | XHR_ACTION: 'xhr',
19 | // Action type for adding a command to the remote clean up queue.
20 | QUEUEADD_ACTION: 'addToQueue',
21 | // Action type for clearing the remote clean up queue.
22 | QUEUECLEAR_ACTION: 'clearQueue',
23 | // Web socket action type specifying that an event occured.
24 | EVENT_ACTION: 'event',
25 |
26 | // Web socket action type to create a remote web socket.
27 | WS_CREATE_ACTION: 'create',
28 | // Web socket event type onerror.
29 | WS_EVENT_ONERROR: 'onerror',
30 | // Web socket event type onmessage.
31 | WS_EVENT_ONMESSAGE: 'onmessage',
32 | // Web socket event type onopen.
33 | WS_EVENT_ONOPEN: 'onopen',
34 | // Web socket event type onclose.
35 | WS_EVENT_ONCLOSE: 'onclose',
36 | // Web socket event sent when an error occurs while calling send.
37 | WS_EVENT_SENDERROR: 'onsenderror',
38 | // Web socket action type to send a message on the remote web socket.
39 | WS_SEND_ACTION: 'send',
40 | // Web socket action type to close the remote web socket.
41 | WS_CLOSE_ACTION: 'close'
42 | };
43 |
--------------------------------------------------------------------------------
/public/html/full_template.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | //
3 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
4 | *
5 | * Use of this source code is governed by a BSD-style license
6 | * that can be found in the LICENSE file in the root of the source
7 | * tree.
8 | html
9 | head
10 | meta(charset='utf-8')
11 | meta(name='description', content='AppRTC: Room full')
12 | meta(name='viewport', content='width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1')
13 | meta(itemprop='description', content='AppRTC: Room full')
14 | meta(itemprop='image', content='/images/webrtc-icon-192x192.png')
15 | meta(itemprop='name', content='AppRTC: Room full')
16 | meta(name='mobile-web-app-capable', content='yes')
17 | meta#theme-color(name='theme-color', content='#ffffff')
18 | base(target='_blank')
19 | title AppRTC: Room full
20 | link(rel='icon', sizes='192x192', href='/images/webrtc-icon-192x192.png')
21 | link(rel='stylesheet', href='/css/main.css')
22 | style.
23 | body {
24 | font-family: "Open Sans";
25 | font-weight: 100;
26 | padding: 25% 0 0 0;
27 | }
28 | button {
29 | display: block;
30 | font-size: 1.5em;
31 | font-family: sans-serif;
32 | margin: 0 auto;
33 | padding: .5em .5em .3em .5em;
34 | width: 12em;
35 | }
36 | div {
37 | color: white;
38 | font-size: 2em;
39 | margin: 0 0 2em 0;
40 | text-align: center;
41 | }
42 | body
43 | div Sorry, this room is full.
44 | button OPEN A NEW ROOM
45 | script.
46 | document.querySelector("button").onclick = function() {
47 | location.href = location.origin;
48 | };
49 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var path = require('path');
3 | var favicon = require('serve-favicon');
4 | var logger = require('morgan');
5 | var cookieParser = require('cookie-parser');
6 | var bodyParser = require('body-parser');
7 |
8 | var routes = require('./routes/index');
9 | var users = require('./routes/users');
10 |
11 | var app = express();
12 |
13 | // view engine setup
14 | app.set('views', path.join(__dirname, 'public/html'));
15 | app.set('view engine', 'jade');
16 |
17 | // uncomment after placing your favicon in /public
18 | //app.use(favicon(__dirname + '/public/favicon.ico'));
19 | app.use(logger('dev'));
20 | app.use(bodyParser.json());
21 | app.use(bodyParser.urlencoded({ extended: false }));
22 | app.use(bodyParser.text());
23 | app.use(cookieParser());
24 | app.use(require('stylus').middleware(path.join(__dirname, 'public')));
25 | app.use(express.static(path.join(__dirname, 'public')));
26 |
27 | app.use('/', routes);
28 | app.use('/users', users);
29 |
30 | // catch 404 and forward to error handler
31 | app.use(function(req, res, next) {
32 | var err = new Error('Not Found');
33 | err.status = 404;
34 | next(err);
35 | });
36 |
37 | // error handlers
38 |
39 | // development error handler
40 | // will print stacktrace
41 | if (app.get('env') === 'development') {
42 | app.use(function(err, req, res, next) {
43 | res.status(err.status || 500);
44 | res.render('error', {
45 | message: err.message,
46 | error: err
47 | });
48 | });
49 | }
50 |
51 | // production error handler
52 | // no stacktraces leaked to user
53 | app.use(function(err, req, res, next) {
54 | res.status(err.status || 500);
55 | res.render('error', {
56 | message: err.message,
57 | error: {}
58 | });
59 | });
60 |
61 |
62 | module.exports = app;
63 |
--------------------------------------------------------------------------------
/public/js/storage.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* exported Storage */
12 | /* globals isChromeApp, chrome */
13 |
14 | 'use strict';
15 |
16 | var Storage = function() {};
17 |
18 | // Get a value from local browser storage. Calls callback with value.
19 | // Handles variation in API between localStorage and Chrome app storage.
20 | Storage.prototype.getStorage = function(key, callback) {
21 | if (isChromeApp()) {
22 | // Use chrome.storage.local.
23 | chrome.storage.local.get(key, function(values) {
24 | // Unwrap key/value pair.
25 | if (callback) {
26 | window.setTimeout(function() {
27 | callback(values[key]);
28 | }, 0);
29 | }
30 | });
31 | } else {
32 | // Use localStorage.
33 | var value = localStorage.getItem(key);
34 | if (callback) {
35 | window.setTimeout(function() {
36 | callback(value);
37 | }, 0);
38 | }
39 | }
40 | };
41 |
42 | // Set a value in local browser storage. Calls callback after completion.
43 | // Handles variation in API between localStorage and Chrome app storage.
44 | Storage.prototype.setStorage = function(key, value, callback) {
45 | if (isChromeApp()) {
46 | // Use chrome.storage.local.
47 | var data = {};
48 | data[key] = value;
49 | chrome.storage.local.set(data, callback);
50 | } else {
51 | // Use localStorage.
52 | localStorage.setItem(key, value);
53 | if (callback) {
54 | window.setTimeout(callback, 0);
55 | }
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('AppRTC-node-server:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/public/js/appcontroller_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals AppController, TestCase, UI_CONSTANTS, assertEquals, assertFalse,
12 | assertTrue, $, RoomSelection:true, Call:true */
13 |
14 | 'use strict';
15 |
16 | var MockRoomSelection = function() {};
17 | MockRoomSelection.RecentlyUsedList = function() {
18 | return {
19 | pushRecentRoom: function() {}
20 | };
21 | };
22 | MockRoomSelection.matchRandomRoomPattern = function() {
23 | return false;
24 | };
25 |
26 | var MockCall = function() {};
27 | MockCall.prototype.start = function() {};
28 | MockCall.prototype.hangup = function() {};
29 |
30 | var AppControllerTest = new TestCase('AppControllerTest');
31 |
32 | AppControllerTest.prototype.setUp = function() {
33 | this.roomSelectionBackup_ = RoomSelection;
34 | RoomSelection = MockRoomSelection;
35 |
36 | this.callBackup_ = Call;
37 | Call = MockCall;
38 |
39 | // Insert mock DOM elements.
40 | for (var key in UI_CONSTANTS) {
41 | var elem = document.createElement('div');
42 | elem.id = UI_CONSTANTS[key].substr(1);
43 | document.body.appendChild(elem);
44 | }
45 |
46 | this.loadingParams_ = {
47 | mediaConstraints: {
48 | audio: true, video: true
49 | }
50 | };
51 | };
52 |
53 | AppControllerTest.prototype.tearDown = function() {
54 | RoomSelection = this.roomSelectionBackup_;
55 | Call = this.callBackup_;
56 | };
57 |
58 | AppControllerTest.prototype.testConfirmToJoin = function() {
59 | this.loadingParams_.roomId = 'myRoom';
60 | new AppController(this.loadingParams_);
61 |
62 | // Verifies that the confirm-to-join UI is visible and the text matches the
63 | // room.
64 | assertEquals(' "' + this.loadingParams_.roomId + '"',
65 | $(UI_CONSTANTS.confirmJoinRoomSpan).textContent);
66 | assertFalse($(UI_CONSTANTS.confirmJoinDiv).classList.contains('hidden'));
67 |
68 | // Verifies that the UI is hidden after clicking the button.
69 | $(UI_CONSTANTS.confirmJoinButton).onclick();
70 | assertTrue($(UI_CONSTANTS.confirmJoinDiv).classList.contains('hidden'));
71 | };
72 |
--------------------------------------------------------------------------------
/public/js/loopback.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* exported setupLoopback */
12 |
13 | 'use strict';
14 |
15 | // We handle the loopback case by making a second connection to the WSS so that
16 | // we receive the same messages that we send out. When receiving an offer we
17 | // convert that offer into an answer message. When receiving candidates we
18 | // echo back the candidate. Answer is ignored because we should never receive
19 | // one while in loopback. Bye is ignored because there is no work to do.
20 | var loopbackWebSocket = null;
21 | var LOOPBACK_CLIENT_ID = 'loopback_client_id';
22 | function setupLoopback(wssUrl, roomId) {
23 | if (loopbackWebSocket) {
24 | return;
25 | }
26 | // TODO(tkchin): merge duplicate code once SignalingChannel abstraction
27 | // exists.
28 | loopbackWebSocket = new WebSocket(wssUrl);
29 |
30 | var sendLoopbackMessage = function(message) {
31 | var msgString = JSON.stringify({
32 | cmd: 'send',
33 | msg: JSON.stringify(message)
34 | });
35 | loopbackWebSocket.send(msgString);
36 | };
37 |
38 | loopbackWebSocket.onopen = function() {
39 | var registerMessage = {
40 | cmd: 'register',
41 | roomid: roomId,
42 | clientid: LOOPBACK_CLIENT_ID
43 | };
44 | loopbackWebSocket.send(JSON.stringify(registerMessage));
45 | };
46 |
47 | loopbackWebSocket.onmessage = function(event) {
48 | var wssMessage;
49 | var message;
50 | try {
51 | wssMessage = JSON.parse(event.data);
52 | message = JSON.parse(wssMessage.msg);
53 | } catch (e) {
54 | trace('Error parsing JSON: ' + event.data);
55 | return;
56 | }
57 | if (wssMessage.error) {
58 | trace('WSS error: ' + wssMessage.error);
59 | return;
60 | }
61 | if (message.type === 'offer') {
62 | var loopbackAnswer = wssMessage.msg;
63 | loopbackAnswer = loopbackAnswer.replace('"offer"', '"answer"');
64 | loopbackAnswer =
65 | loopbackAnswer.replace('a=ice-options:google-ice\\r\\n', '');
66 | sendLoopbackMessage(JSON.parse(loopbackAnswer));
67 | } else if (message.type === 'candidate') {
68 | sendLoopbackMessage(message);
69 | }
70 | };
71 |
72 | loopbackWebSocket.onclose = function(event) {
73 | trace('Loopback closed with code:' + event.code + ' reason:' +
74 | event.reason);
75 | // TODO(tkchin): try to reconnect.
76 | loopbackWebSocket = null;
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/public/js/appwindow.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 | // Variables defined in and used from main.js.
11 | /* globals randomString, AppController, sendAsyncUrlRequest, parseJSON */
12 | /* exported params */
13 | 'use strict';
14 |
15 | // Generate random room id and connect.
16 |
17 | var roomServer = 'https://apprtc.appspot.com';
18 | var loadingParams = {
19 | errorMessages: [],
20 | suggestedRoomId: randomString(9),
21 | roomServer: roomServer,
22 | connect: false,
23 | paramsFunction: function() {
24 | return new Promise(function(resolve, reject) {
25 | trace('Initializing; retrieving params from: ' + roomServer + '/params');
26 | sendAsyncUrlRequest('GET', roomServer + '/params').then(function(result) {
27 | var serverParams = parseJSON(result);
28 | var newParams = {};
29 | if (!serverParams) {
30 | resolve(newParams);
31 | return;
32 | }
33 |
34 | // Convert from server format to expected format.
35 | // TODO(tkchin): clean up response format. JSHint doesn't like it.
36 | /* jshint ignore:start */
37 | //jscs:disable requireCamelCaseOrUpperCaseIdentifiers
38 | newParams.isLoopback = serverParams.is_loopback === 'true';
39 | newParams.mediaConstraints = parseJSON(serverParams.media_constraints);
40 | newParams.offerConstraints = parseJSON(serverParams.offer_constraints);
41 | newParams.peerConnectionConfig = parseJSON(serverParams.pc_config);
42 | newParams.peerConnectionConstraints =
43 | parseJSON(serverParams.pc_constraints);
44 | newParams.turnRequestUrl = serverParams.turn_url;
45 | newParams.turnTransports = serverParams.turn_transports;
46 | newParams.wssUrl = serverParams.wss_url;
47 | newParams.wssPostUrl = serverParams.wss_post_url;
48 | newParams.versionInfo = parseJSON(serverParams.version_info);
49 | //jscs:enable requireCamelCaseOrUpperCaseIdentifiers
50 | /* jshint ignore:end */
51 | newParams.messages = serverParams.messages;
52 |
53 | trace('Initializing; parameters from server: ');
54 | trace(JSON.stringify(newParams));
55 | resolve(newParams);
56 | }).catch(function(error) {
57 | trace('Initializing; error getting params from server: ' +
58 | error.message);
59 | reject(error);
60 | });
61 | });
62 | }
63 | };
64 |
65 | new AppController(loadingParams);
66 |
--------------------------------------------------------------------------------
/public/js/remotewebsocket.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals apprtc, Constants */
12 | /* exported RemoteWebSocket */
13 |
14 | 'use strict';
15 |
16 | // This class is used as a proxy for the WebSocket owned by background.js.
17 | // This proxy class sends commands and receives events via a Port object
18 | // opened to communicate with background.js in a Chrome App.
19 | // The WebSocket object must be owned by background.js so the call can be
20 | // properly terminated when the app window is closed.
21 | var RemoteWebSocket = function(wssUrl, wssPostUrl) {
22 | this.wssUrl_ = wssUrl;
23 | apprtc.windowPort.addMessageListener(this.handleMessage_.bind(this));
24 | this.sendMessage_({
25 | action: Constants.WS_ACTION,
26 | wsAction: Constants.WS_CREATE_ACTION,
27 | wssUrl: wssUrl,
28 | wssPostUrl: wssPostUrl
29 | });
30 | this.readyState = WebSocket.CONNECTING;
31 | };
32 |
33 | RemoteWebSocket.prototype.sendMessage_ = function(message) {
34 | apprtc.windowPort.sendMessage(message);
35 | };
36 |
37 | RemoteWebSocket.prototype.send = function(data) {
38 | if (this.readyState !== WebSocket.OPEN) {
39 | throw 'Web socket is not in OPEN state: ' + this.readyState;
40 | }
41 | this.sendMessage_({
42 | action: Constants.WS_ACTION,
43 | wsAction: Constants.WS_SEND_ACTION,
44 | data: data
45 | });
46 | };
47 |
48 | RemoteWebSocket.prototype.close = function() {
49 | if (this.readyState === WebSocket.CLOSING ||
50 | this.readyState === WebSocket.CLOSED) {
51 | return;
52 | }
53 | this.readyState = WebSocket.CLOSING;
54 | this.sendMessage_({
55 | action: Constants.WS_ACTION,
56 | wsAction: Constants.WS_CLOSE_ACTION
57 | });
58 | };
59 |
60 | RemoteWebSocket.prototype.handleMessage_ = function(message) {
61 | if (message.action === Constants.WS_ACTION &&
62 | message.wsAction === Constants.EVENT_ACTION) {
63 | if (message.wsEvent === Constants.WS_EVENT_ONOPEN) {
64 | this.readyState = WebSocket.OPEN;
65 | if (this.onopen) {
66 | this.onopen();
67 | }
68 | } else if (message.wsEvent === Constants.WS_EVENT_ONCLOSE) {
69 | this.readyState = WebSocket.CLOSED;
70 | if (this.onclose) {
71 | this.onclose(message.data);
72 | }
73 | } else if (message.wsEvent === Constants.WS_EVENT_ONERROR) {
74 | if (this.onerror) {
75 | this.onerror(message.data);
76 | }
77 | } else if (message.wsEvent === Constants.WS_EVENT_ONMESSAGE) {
78 | if (this.onmessage) {
79 | this.onmessage(message.data);
80 | }
81 | } else if (message.wsEvent === Constants.WS_EVENT_SENDERROR) {
82 | if (this.onsenderror) {
83 | this.onsenderror(message.data);
84 | }
85 | trace('ERROR: web socket send failed: ' + message.data);
86 | }
87 | }
88 | };
89 |
--------------------------------------------------------------------------------
/lib/rooms.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Manages a user in a chat session
3 | * @param isInitiator
4 | * determines if the user is the initiator/host of
5 | * the chat
6 | * @constructor
7 | */
8 | Client = function(isInitiator) {
9 | var self = this;
10 | this.isInitiator = isInitiator;
11 | this.messages = [];
12 |
13 | this.addMessage = function(message) {
14 | self.messages.push(message);
15 | };
16 |
17 | this.clearMessages = function() {
18 | self.messages = [];
19 | };
20 |
21 | this.toString = function() {
22 | return '{ '+ self.isInitiator +', '+ self.messages.length +' }';
23 | }
24 | }
25 |
26 | /**
27 | * Manges a video chat session containing information about
28 | * the 2 users in a chat.
29 | * @constructor
30 | */
31 | Room = function() {
32 | var self = this;
33 | var clientMap = {}; //map of key/value pair for client objects
34 |
35 | this.getOccupancy = function() {
36 | var keys = Object.keys(clientMap);
37 | return keys.length;
38 | };
39 |
40 | this.hasClient = function(clientId) {
41 | return clientMap[clientId];
42 | }
43 |
44 | this.join = function(clientId, callback) {
45 | var clientIds = Object.keys(clientMap);
46 | var otherClient = clientIds.length > 0 ? clientMap[clientIds[0]] : null;
47 | var isInitiator = otherClient == null;
48 | var client = new Client(isInitiator);
49 | clientMap[clientId] = client;
50 | if (callback) callback(null, client, otherClient);
51 | };
52 |
53 | this.removeClient = function(clientId, callback) {
54 | delete clientMap[clientId];
55 | var otherClient = clientIds.length > 0 ? clientMap[clientIds[0]] : null;
56 | callback(null, true, otherClient);
57 | };
58 |
59 | this.getClient = function(clientId) {
60 | return clientMap[clientId];
61 | }
62 |
63 | this.toString = function() {
64 | return JSON.stringify(Object.keys(clientMap));
65 | };
66 | };
67 |
68 | /**
69 | * Manages a collection of rooms to maintain video chat sessions.
70 | * The purpose of this class is to allow overloading this class
71 | * to implement memcache or redis cluser for handling large scale
72 | * concurrent video chat sessions.
73 | * @constructor
74 | */
75 | Rooms = function() {
76 | var self = this;
77 | var roomMap = {}; //map of key/value pair for room objects
78 |
79 | this.get = function(roomCacheKey, callback) {
80 | var room = roomMap[roomCacheKey];
81 | callback(null, room);
82 | };
83 |
84 | this.create = function(roomCacheKey, callback) {
85 | var room = new Room;
86 | roomMap[roomCacheKey] = room;
87 | callback(null, room);
88 | };
89 |
90 | this.createIfNotExist = function(roomCacheKey, callback) {
91 | self.get(roomCacheKey, function(error, room) {
92 | if (error) {
93 | callback(error);
94 | return;
95 | }
96 | if (!room) {
97 | self.create(roomCacheKey, callback);
98 | } else {
99 | callback(error, room);
100 | }
101 | });
102 | }
103 | };
104 |
105 | module.exports = Rooms;
--------------------------------------------------------------------------------
/public/js/test_mocks.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals assertEquals */
12 | /* exported FAKE_WSS_POST_URL, FAKE_WSS_URL, FAKE_WSS_POST_URL, FAKE_ROOM_ID,
13 | FAKE_CLIENT_ID, MockWebSocket, MockXMLHttpRequest, webSockets, xhrs,
14 | MockWindowPort, FAKE_SEND_EXCEPTION */
15 |
16 | 'use strict';
17 |
18 | var FAKE_WSS_URL = 'wss://foo.com';
19 | var FAKE_WSS_POST_URL = 'https://foo.com';
20 | var FAKE_ROOM_ID = 'bar';
21 | var FAKE_CLIENT_ID = 'barbar';
22 | var FAKE_SEND_EXCEPTION = 'Send exception';
23 |
24 | var webSockets = [];
25 | var MockWebSocket = function(url) {
26 | assertEquals(FAKE_WSS_URL, url);
27 |
28 | this.url = url;
29 | this.messages = [];
30 | this.readyState = WebSocket.CONNECTING;
31 |
32 | this.onopen = null;
33 | this.onclose = null;
34 | this.onerror = null;
35 | this.onmessage = null;
36 |
37 | webSockets.push(this);
38 | };
39 |
40 | MockWebSocket.CONNECTING = WebSocket.CONNECTING;
41 | MockWebSocket.OPEN = WebSocket.OPEN;
42 | MockWebSocket.CLOSED = WebSocket.CLOSED;
43 |
44 | MockWebSocket.prototype.simulateOpenResult = function(success) {
45 | if (success) {
46 | this.readyState = WebSocket.OPEN;
47 | if (this.onopen) {
48 | this.onopen();
49 | }
50 | } else {
51 | this.readyState = WebSocket.CLOSED;
52 | if (this.onerror) {
53 | this.onerror(Error('Mock open error'));
54 | }
55 | }
56 | };
57 |
58 | MockWebSocket.prototype.send = function(msg) {
59 | if (this.readyState !== WebSocket.OPEN) {
60 | throw 'Send called when the connection is not open';
61 | }
62 |
63 | if (this.throwOnSend) {
64 | throw FAKE_SEND_EXCEPTION;
65 | }
66 |
67 | this.messages.push(msg);
68 | };
69 |
70 | MockWebSocket.prototype.close = function() {
71 | this.readyState = WebSocket.CLOSED;
72 | };
73 |
74 | var xhrs = [];
75 | var MockXMLHttpRequest = function() {
76 | this.url = null;
77 | this.method = null;
78 | this.async = true;
79 | this.body = null;
80 | this.readyState = 0;
81 |
82 | xhrs.push(this);
83 | };
84 | MockXMLHttpRequest.prototype.open = function(method, path, async) {
85 | this.url = path;
86 | this.method = method;
87 | this.async = async;
88 | this.readyState = 1;
89 | };
90 | MockXMLHttpRequest.prototype.send = function(body) {
91 | this.body = body;
92 | if (this.async) {
93 | this.readyState = 2;
94 | } else {
95 | this.readyState = 4;
96 | }
97 | };
98 |
99 | var MockWindowPort = function() {
100 | this.messages = [];
101 | this.onMessage_ = null;
102 | };
103 |
104 | MockWindowPort.prototype.addMessageListener = function(callback) {
105 | this.onMessage_ = callback;
106 | };
107 |
108 | MockWindowPort.prototype.sendMessage = function(message) {
109 | this.messages.push(message);
110 | };
111 |
112 | MockWindowPort.prototype.simulateMessageFromBackground = function(message) {
113 | if (this.onMessage_) {
114 | this.onMessage_(message);
115 | }
116 | };
117 |
--------------------------------------------------------------------------------
/public/js/stats.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* exported computeBitrate, computeE2EDelay, computeRate,
12 | extractStatAsInt, refreshStats */
13 |
14 | 'use strict';
15 |
16 | // Return the integer stat |statName| from the object with type |statObj| in
17 | // |stats|, or null if not present.
18 | function extractStatAsInt(stats, statObj, statName) {
19 | // Ignore stats that have a 'nullish' value.
20 | // The correct fix is indicated in
21 | // https://code.google.com/p/webrtc/issues/detail?id=3377.
22 | var str = extractStat(stats, statObj, statName);
23 | if (str) {
24 | var val = parseInt(str);
25 | if (val !== -1) {
26 | return val;
27 | }
28 | }
29 | return null;
30 | }
31 |
32 | // Return the stat |statName| from the object with type |statObj| in |stats|
33 | // as a string, or null if not present.
34 | function extractStat(stats, statObj, statName) {
35 | var report = getStatsReport(stats, statObj, statName);
36 | if (report && report.names().indexOf(statName) !== -1) {
37 | return report.stat(statName);
38 | }
39 | return null;
40 | }
41 |
42 | // Return the stats report with type |statObj| in |stats|, with the stat
43 | // |statName| (if specified), and value |statVal| (if specified). Return
44 | // undef if not present.
45 | function getStatsReport(stats, statObj, statName, statVal) {
46 | if (stats) {
47 | for (var i = 0; i < stats.length; ++i) {
48 | var report = stats[i];
49 | if (report.type === statObj) {
50 | var found = true;
51 | // If |statName| is present, ensure |report| has that stat.
52 | // If |statVal| is present, ensure the value matches.
53 | if (statName) {
54 | var val = report.stat(statName);
55 | found = (statVal !== undefined) ? (val === statVal) : val;
56 | }
57 | if (found) {
58 | return report;
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
65 | // Takes two stats reports and determines the rate based on two counter readings
66 | // and the time between them (which is in units of milliseconds).
67 | function computeRate(newReport, oldReport, statName) {
68 | var newVal = newReport.stat(statName);
69 | var oldVal = (oldReport) ? oldReport.stat(statName) : null;
70 | if (newVal === null || oldVal === null) {
71 | return null;
72 | }
73 | return (newVal - oldVal) / (newReport.timestamp - oldReport.timestamp) * 1000;
74 | }
75 |
76 | // Convert a byte rate to a bit rate.
77 | function computeBitrate(newReport, oldReport, statName) {
78 | return computeRate(newReport, oldReport, statName) * 8;
79 | }
80 |
81 | // Computes end to end delay based on the capture start time (in NTP format)
82 | // and the current render time (in seconds since start of render).
83 | function computeE2EDelay(captureStart, remoteVideoCurrentTime) {
84 | if (!captureStart) {
85 | return null;
86 | }
87 |
88 | // Adding offset (milliseconds between 1900 and 1970) to get NTP time.
89 | var nowNTP = Date.now() + 2208988800000;
90 | return nowNTP - captureStart - remoteVideoCurrentTime * 1000;
91 | }
92 |
--------------------------------------------------------------------------------
/public/js/utils_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals TestCase, filterTurnUrls, assertEquals, randomString,
12 | queryStringToDictionary */
13 |
14 | 'use strict';
15 |
16 | var TURN_URLS = [
17 | 'turn:turn.example.com?transport=tcp',
18 | 'turn:turn.example.com?transport=udp',
19 | 'turn:turn.example.com:8888?transport=udp',
20 | 'turn:turn.example.com:8888?transport=tcp'
21 | ];
22 |
23 | var TURN_URLS_UDP = [
24 | 'turn:turn.example.com?transport=udp',
25 | 'turn:turn.example.com:8888?transport=udp',
26 | ];
27 |
28 | var TURN_URLS_TCP = [
29 | 'turn:turn.example.com?transport=tcp',
30 | 'turn:turn.example.com:8888?transport=tcp'
31 | ];
32 |
33 | var UtilsTest = new TestCase('UtilsTest');
34 |
35 | UtilsTest.prototype.testFilterTurnUrlsUdp = function() {
36 | var urls = TURN_URLS.slice(0); // make a copy
37 | filterTurnUrls(urls, 'udp');
38 | assertEquals('Only transport=udp URLs should remain.', TURN_URLS_UDP, urls);
39 | };
40 |
41 | UtilsTest.prototype.testFilterTurnUrlsTcp = function() {
42 | var urls = TURN_URLS.slice(0); // make a copy
43 | filterTurnUrls(urls, 'tcp');
44 | assertEquals('Only transport=tcp URLs should remain.', TURN_URLS_TCP, urls);
45 | };
46 |
47 | UtilsTest.prototype.testRandomReturnsCorrectLength = function() {
48 | assertEquals('13 length string', 13, randomString(13).length);
49 | assertEquals('5 length string', 5, randomString(5).length);
50 | assertEquals('10 length string', 10, randomString(10).length);
51 | };
52 |
53 | UtilsTest.prototype.testRandomReturnsCorrectCharacters = function() {
54 | var str = randomString(500);
55 |
56 | // randromString should return only the digits 0-9.
57 | var positiveRe = /^[0-9]+$/;
58 | var negativeRe = /[^0-9]/;
59 |
60 | var positiveResult = positiveRe.exec(str);
61 | var negativeResult = negativeRe.exec(str);
62 |
63 | assertEquals(
64 | 'Number only regular expression should match.',
65 | 0, positiveResult.index);
66 | assertEquals(
67 | 'Anything other than digits regular expression should not match.',
68 | null, negativeResult);
69 | };
70 |
71 | UtilsTest.prototype.testQueryStringToDictionary = function() {
72 | var dictionary = {
73 | 'foo': 'a',
74 | 'baz': '',
75 | 'bar': 'b',
76 | 'tee': '',
77 | };
78 |
79 | var buildQuery = function(data, includeEqualsOnEmpty) {
80 | var queryString = '?';
81 | for (var key in data) {
82 | queryString += key;
83 | if (data[key] || includeEqualsOnEmpty) {
84 | queryString += '=';
85 | }
86 | queryString += data[key] + '&';
87 | }
88 | queryString = queryString.slice(0, -1);
89 | return queryString;
90 | };
91 |
92 | // Build query where empty value is formatted as &tee=&.
93 | var query = buildQuery(dictionary, true);
94 | var result = queryStringToDictionary(query);
95 | assertEquals(JSON.stringify(dictionary), JSON.stringify(result));
96 |
97 | // Build query where empty value is formatted as &tee&.
98 | query = buildQuery(dictionary, false);
99 | result = queryStringToDictionary(query);
100 | assertEquals(JSON.stringify(dictionary), JSON.stringify(result));
101 |
102 | result = queryStringToDictionary('?');
103 | assertEquals(0, Object.keys(result).length);
104 |
105 | result = queryStringToDictionary('?=');
106 | assertEquals(0, Object.keys(result).length);
107 |
108 | result = queryStringToDictionary('?&=');
109 | assertEquals(0, Object.keys(result).length);
110 |
111 | result = queryStringToDictionary('');
112 | assertEquals(0, Object.keys(result).length);
113 |
114 | result = queryStringToDictionary('?=abc');
115 | assertEquals(0, Object.keys(result).length);
116 | };
117 |
--------------------------------------------------------------------------------
/public/js/sdputils_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals TestCase, maybePreferCodec, removeCodecParam, setCodecParam,
12 | assertEquals */
13 |
14 | 'use strict';
15 |
16 | var SDP_WITH_AUDIO_CODECS =
17 | ['v=0',
18 | 'm=audio 9 RTP/SAVPF 111 103 104 0 9',
19 | 'a=rtcp-mux',
20 | 'a=rtpmap:111 opus/48000/2',
21 | 'a=fmtp:111 minptime=10',
22 | 'a=rtpmap:103 ISAC/16000',
23 | 'a=rtpmap:9 G722/8000',
24 | 'a=rtpmap:0 PCMU/8000',
25 | 'a=rtpmap:8 PCMA/8000',
26 | ].join('\r\n');
27 |
28 | var SdpUtilsTest = new TestCase('SdpUtilsTest');
29 |
30 | SdpUtilsTest.prototype.testMovesIsac16KToDefaultWhenPreferred = function() {
31 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 'audio', 'send',
32 | 'iSAC/16000');
33 | var audioLine = result.split('\r\n')[1];
34 | assertEquals('iSAC 16K (of type 103) should be moved to front.',
35 | 'm=audio 9 RTP/SAVPF 103 111 104 0 9',
36 | audioLine);
37 | };
38 |
39 | SdpUtilsTest.prototype.testDoesNothingIfPreferredCodecNotFound = function() {
40 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 'audio', 'send',
41 | 'iSAC/123456');
42 | var audioLine = result.split('\r\n')[1];
43 | assertEquals('SDP should be unaffected since the codec does not exist.',
44 | SDP_WITH_AUDIO_CODECS.split('\r\n')[1],
45 | audioLine);
46 | };
47 |
48 | SdpUtilsTest.prototype.testMovesCodecEvenIfPayloadTypeIsSameAsUdpPort =
49 | function() {
50 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS,
51 | 'audio',
52 | 'send',
53 | 'G722/8000');
54 | var audioLine = result.split('\r\n')[1];
55 | assertEquals('G722/8000 (of type 9) should be moved to front.',
56 | 'm=audio 9 RTP/SAVPF 9 111 103 104 0',
57 | audioLine);
58 | };
59 |
60 | SdpUtilsTest.prototype.testRemoveAndSetCodecParamModifyFmtpLine =
61 | function() {
62 | var result = setCodecParam(SDP_WITH_AUDIO_CODECS, 'opus/48000',
63 | 'minptime', '20');
64 | var audioLine = result.split('\r\n')[4];
65 | assertEquals('minptime=10 should be modified in a=fmtp:111 line.',
66 | 'a=fmtp:111 minptime=20', audioLine);
67 |
68 | result = setCodecParam(result, 'opus/48000', 'useinbandfec', '1');
69 | audioLine = result.split('\r\n')[4];
70 | assertEquals('useinbandfec=1 should be added to a=fmtp:111 line.',
71 | 'a=fmtp:111 minptime=20; useinbandfec=1', audioLine);
72 |
73 | result = removeCodecParam(result, 'opus/48000', 'minptime');
74 | audioLine = result.split('\r\n')[4];
75 | assertEquals('minptime should be removed from a=fmtp:111 line.',
76 | 'a=fmtp:111 useinbandfec=1', audioLine);
77 |
78 | var newResult = removeCodecParam(result, 'opus/48000', 'minptime');
79 | assertEquals('removeCodecParam should not affect sdp ' +
80 | 'if param did not exist', result, newResult);
81 | };
82 |
83 | SdpUtilsTest.prototype.testRemoveAndSetCodecParamRemoveAndAddFmtpLineIfNeeded =
84 | function() {
85 | var result = removeCodecParam(SDP_WITH_AUDIO_CODECS, 'opus/48000',
86 | 'minptime');
87 | var audioLine = result.split('\r\n')[4];
88 | assertEquals('a=fmtp:111 line should be deleted.',
89 | 'a=rtpmap:103 ISAC/16000', audioLine);
90 | result = setCodecParam(result, 'opus/48000', 'inbandfec', '1');
91 | audioLine = result.split('\r\n')[4];
92 | assertEquals('a=fmtp:111 line should be added.',
93 | 'a=fmtp:111 inbandfec=1', audioLine);
94 | };
95 |
--------------------------------------------------------------------------------
/public/js/testpolyfills.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | 'use strict';
12 |
13 | Function.prototype.bind = Function.prototype.bind || function(thisp) {
14 | var fn = this;
15 | var suppliedArgs = Array.prototype.slice.call(arguments, 1);
16 | return function() {
17 | return fn.apply(thisp,
18 | suppliedArgs.concat(Array.prototype.slice.call(arguments)));
19 | };
20 | };
21 |
22 | if (!window.performance) {
23 | window.performance = function() {};
24 | window.performance.now = function() { return 0; };
25 | }
26 |
27 | window.RTCSessionDescription = window.RTCSessionDescription || function(input) {
28 | this.type = input.type;
29 | this.sdp = input.sdp;
30 | };
31 |
32 | window.RTCIceCandidate = window.RTCIceCandidate || function(candidate) {
33 | this.sdpMLineIndex = candidate.sdpMLineIndex;
34 | this.candidate = candidate.candidate;
35 | };
36 |
37 | var PROMISE_STATE = {
38 | PENDING: 0,
39 | FULLFILLED: 1,
40 | REJECTED: 2
41 | };
42 |
43 | var MyPromise = function(executor) {
44 | this.state_ = PROMISE_STATE.PENDING;
45 | this.resolveCallback_ = null;
46 | this.rejectCallback_ = null;
47 |
48 | this.value_ = null;
49 | this.reason_ = null;
50 | executor(this.onResolve_.bind(this), this.onReject_.bind(this));
51 | };
52 |
53 | MyPromise.all = function(promises) {
54 | var values = new Array(promises.length);
55 | return new MyPromise(function(values, resolve, reject) {
56 | function onResolve(values, index, value) {
57 | values[index] = value || null;
58 |
59 | for (var i = 0; i < values.length; ++i) {
60 | if (values[i] === undefined) {
61 | return;
62 | }
63 | }
64 | resolve(values);
65 | }
66 | for (var i = 0; i < promises.length; ++i) {
67 | promises[i].then(onResolve.bind(null, values, i), reject);
68 | }
69 | }.bind(null, values));
70 | };
71 |
72 | MyPromise.resolve = function(value) {
73 | return new MyPromise(function(resolve) {
74 | resolve(value);
75 | });
76 | };
77 |
78 | MyPromise.reject = function(error) {
79 | // JSHint flags the unused variable resolve.
80 | return new MyPromise(function(resolve, reject) { // jshint ignore:line
81 | reject(error);
82 | });
83 | };
84 |
85 | MyPromise.prototype.then = function(onResolve, onReject) {
86 | switch (this.state_) {
87 | case PROMISE_STATE.PENDING:
88 | this.resolveCallback_ = onResolve;
89 | this.rejectCallback_ = onReject;
90 | break;
91 | case PROMISE_STATE.FULLFILLED:
92 | onResolve(this.value_);
93 | break;
94 | case PROMISE_STATE.REJECTED:
95 | if (onReject) {
96 | onReject(this.reason_);
97 | }
98 | break;
99 | }
100 | return this;
101 | };
102 |
103 | MyPromise.prototype.catch = function(onReject) {
104 | switch (this.state_) {
105 | case PROMISE_STATE.PENDING:
106 | this.rejectCallback_ = onReject;
107 | break;
108 | case PROMISE_STATE.FULLFILLED:
109 | break;
110 | case PROMISE_STATE.REJECTED:
111 | onReject(this.reason_);
112 | break;
113 | }
114 | return this;
115 | };
116 |
117 | MyPromise.prototype.onResolve_ = function(value) {
118 | if (this.state_ !== PROMISE_STATE.PENDING) {
119 | return;
120 | }
121 | this.state_ = PROMISE_STATE.FULLFILLED;
122 | if (this.resolveCallback_) {
123 | this.resolveCallback_(value);
124 | } else {
125 | this.value_ = value;
126 | }
127 | };
128 |
129 | MyPromise.prototype.onReject_ = function(reason) {
130 | if (this.state_ !== PROMISE_STATE.PENDING) {
131 | return;
132 | }
133 | this.state_ = PROMISE_STATE.REJECTED;
134 | if (this.rejectCallback_) {
135 | this.rejectCallback_(reason);
136 | } else {
137 | this.reason_ = reason;
138 | }
139 | };
140 |
141 | window.Promise = window.Promise || MyPromise;
142 |
143 | // Provide a shim for phantomjs, where chrome is not defined.
144 | var myChrome = (function() {
145 | var onConnectCallback_;
146 | return {
147 | app: {
148 | runtime: {
149 | onLaunched: {
150 | addListener: function(callback) {
151 | console.log(
152 | 'chrome.app.runtime.onLaunched.addListener called:' + callback);
153 | }
154 | }
155 | },
156 | window: {
157 | create: function(fileName, callback) {
158 | console.log(
159 | 'chrome.window.create called: ' +
160 | fileName + ', ' + callback);
161 | }
162 | }
163 | },
164 | runtime: {
165 | onConnect: {
166 | addListener: function(callback) {
167 | console.log(
168 | 'chrome.runtime.onConnect.addListener called: ' + callback);
169 | onConnectCallback_ = callback;
170 | }
171 | }
172 | },
173 | callOnConnect: function(port) {
174 | if (onConnectCallback_) {
175 | onConnectCallback_(port);
176 | }
177 | }
178 | };
179 | })();
180 |
181 | window.chrome = window.chrome || myChrome;
182 |
--------------------------------------------------------------------------------
/public/js/signalingchannel.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals parseJSON, trace, sendUrlRequest, isChromeApp, RemoteWebSocket */
12 | /* exported SignalingChannel */
13 |
14 | 'use strict';
15 |
16 | // This class implements a signaling channel based on WebSocket.
17 | var SignalingChannel = function(wssUrl, wssPostUrl) {
18 | this.wssUrl_ = wssUrl;
19 | this.wssPostUrl_ = wssPostUrl;
20 | this.roomId_ = null;
21 | this.clientId_ = null;
22 | this.websocket_ = null;
23 | this.registered_ = false;
24 |
25 | // Public callbacks. Keep it sorted.
26 | this.onerror = null;
27 | this.onmessage = null;
28 | };
29 |
30 | SignalingChannel.prototype.open = function() {
31 | if (this.websocket_) {
32 | trace('ERROR: SignalingChannel has already opened.');
33 | return;
34 | }
35 |
36 | trace('Opening signaling channel.');
37 | return new Promise(function(resolve, reject) {
38 | if (isChromeApp()) {
39 | this.websocket_ = new RemoteWebSocket(this.wssUrl_, this.wssPostUrl_);
40 | } else {
41 | this.websocket_ = new WebSocket(this.wssUrl_);
42 | }
43 |
44 | this.websocket_.onopen = function() {
45 | trace('Signaling channel opened.');
46 |
47 | this.websocket_.onerror = function() {
48 | trace('Signaling channel error.');
49 | };
50 | this.websocket_.onclose = function(event) {
51 | // TODO(tkchin): reconnect to WSS.
52 | trace('Channel closed with code:' + event.code +
53 | ' reason:' + event.reason);
54 | this.websocket_ = null;
55 | this.registered_ = false;
56 | };
57 |
58 | if (this.clientId_ && this.roomId_) {
59 | this.register(this.roomId_, this.clientId_);
60 | }
61 |
62 | resolve();
63 | }.bind(this);
64 |
65 | this.websocket_.onmessage = function(event) {
66 | trace('WSS->C: ' + event.data);
67 |
68 | var message = parseJSON(event.data);
69 | if (!message) {
70 | trace('Failed to parse WSS message: ' + event.data);
71 | return;
72 | }
73 | if (message.error) {
74 | trace('Signaling server error message: ' + message.error);
75 | return;
76 | }
77 | this.onmessage(message.msg);
78 | }.bind(this);
79 |
80 | this.websocket_.onerror = function() {
81 | reject(Error('WebSocket error.'));
82 | };
83 | }.bind(this));
84 | };
85 |
86 | SignalingChannel.prototype.register = function(roomId, clientId) {
87 | if (this.registered_) {
88 | trace('ERROR: SignalingChannel has already registered.');
89 | return;
90 | }
91 |
92 | this.roomId_ = roomId;
93 | this.clientId_ = clientId;
94 |
95 | if (!this.roomId_) {
96 | trace('ERROR: missing roomId.');
97 | }
98 | if (!this.clientId_) {
99 | trace('ERROR: missing clientId.');
100 | }
101 | if (!this.websocket_ || this.websocket_.readyState !== WebSocket.OPEN) {
102 | trace('WebSocket not open yet; saving the IDs to register later.');
103 | return;
104 | }
105 | trace('Registering signaling channel.');
106 | var registerMessage = {
107 | cmd: 'register',
108 | roomid: this.roomId_,
109 | clientid: this.clientId_
110 | };
111 | this.websocket_.send(JSON.stringify(registerMessage));
112 | this.registered_ = true;
113 |
114 | // TODO(tkchin): Better notion of whether registration succeeded. Basically
115 | // check that we don't get an error message back from the socket.
116 | trace('Signaling channel registered.');
117 | };
118 |
119 | SignalingChannel.prototype.close = function(async) {
120 | if (this.websocket_) {
121 | this.websocket_.close();
122 | this.websocket_ = null;
123 | }
124 |
125 | if (!this.clientId_ || !this.roomId_) {
126 | return;
127 | }
128 | // Tell WSS that we're done.
129 | var path = this.getWssPostUrl();
130 |
131 | return sendUrlRequest('DELETE', path, async).catch(function(error) {
132 | trace('Error deleting web socket connection: ' + error.message);
133 | }.bind(this)).then(function() {
134 | this.clientId_ = null;
135 | this.roomId_ = null;
136 | this.registered_ = false;
137 | }.bind(this));
138 | };
139 |
140 | SignalingChannel.prototype.send = function(message) {
141 | if (!this.roomId_ || !this.clientId_) {
142 | trace('ERROR: SignalingChannel has not registered.');
143 | return;
144 | }
145 | trace('C->WSS: ' + message);
146 |
147 | var wssMessage = {
148 | cmd: 'send',
149 | msg: message
150 | };
151 | var msgString = JSON.stringify(wssMessage);
152 |
153 | if (this.websocket_ && this.websocket_.readyState === WebSocket.OPEN) {
154 | this.websocket_.send(msgString);
155 | } else {
156 | var path = this.getWssPostUrl();
157 | var xhr = new XMLHttpRequest();
158 | xhr.open('POST', path, true);
159 | xhr.send(wssMessage.msg);
160 | }
161 | };
162 |
163 | SignalingChannel.prototype.getWssPostUrl = function() {
164 | return this.wssPostUrl_ + '/' + this.roomId_ + '/' + this.clientId_;
165 | };
166 |
--------------------------------------------------------------------------------
/public/html/params.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | AppRTC parameters
13 |
14 |
15 |
16 |
17 |
18 |
47 |
48 |
49 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/public/js/remotewebsocket_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals TestCase, assertEquals, Constants, FAKE_WSS_URL, apprtc,
12 | RemoteWebSocket, MockWindowPort */
13 |
14 | 'use strict';
15 | var TEST_MESSAGE = 'foobar';
16 |
17 | var RemoteWebSocketTest = new TestCase('RemoteWebSocketTest');
18 |
19 | RemoteWebSocketTest.prototype.setUp = function() {
20 | this.realWindowPort = apprtc.windowPort;
21 | apprtc.windowPort = new MockWindowPort();
22 |
23 | this.rws_ = new RemoteWebSocket(FAKE_WSS_URL);
24 | // Should have an message to request create.
25 | assertEquals(1, apprtc.windowPort.messages.length);
26 | assertEquals(Constants.WS_ACTION, apprtc.windowPort.messages[0].action);
27 | assertEquals(Constants.WS_CREATE_ACTION,
28 | apprtc.windowPort.messages[0].wsAction);
29 | assertEquals(FAKE_WSS_URL, apprtc.windowPort.messages[0].wssUrl);
30 | assertEquals(WebSocket.CONNECTING, this.rws_.readyState);
31 |
32 | };
33 |
34 | RemoteWebSocketTest.prototype.tearDown = function() {
35 | apprtc.windowPort = this.realWindowPort;
36 | };
37 |
38 | RemoteWebSocketTest.prototype.testSendBeforeOpen = function() {
39 | var exception = false;
40 | try {
41 | this.rws_.send(TEST_MESSAGE);
42 | } catch (ex) {
43 | if (ex) {
44 | exception = true;
45 | }
46 | }
47 |
48 | assertEquals(true, exception);
49 | };
50 |
51 | RemoteWebSocketTest.prototype.testSend = function() {
52 | apprtc.windowPort.simulateMessageFromBackground({
53 | action: Constants.WS_ACTION,
54 | wsAction: Constants.EVENT_ACTION,
55 | wsEvent: Constants.WS_EVENT_ONOPEN,
56 | data: TEST_MESSAGE
57 | });
58 |
59 | assertEquals(1, apprtc.windowPort.messages.length);
60 | this.rws_.send(TEST_MESSAGE);
61 | assertEquals(2, apprtc.windowPort.messages.length);
62 | assertEquals(Constants.WS_ACTION, apprtc.windowPort.messages[1].action);
63 | assertEquals(Constants.WS_SEND_ACTION,
64 | apprtc.windowPort.messages[1].wsAction);
65 | assertEquals(TEST_MESSAGE, apprtc.windowPort.messages[1].data);
66 | };
67 |
68 | RemoteWebSocketTest.prototype.testClose = function() {
69 | var message = null;
70 | var called = false;
71 | this.rws_.onclose = function(e) {
72 | called = true;
73 | message = e;
74 | };
75 |
76 | assertEquals(1, apprtc.windowPort.messages.length);
77 | this.rws_.close();
78 |
79 | assertEquals(2, apprtc.windowPort.messages.length);
80 | assertEquals(Constants.WS_ACTION, apprtc.windowPort.messages[1].action);
81 | assertEquals(Constants.WS_CLOSE_ACTION,
82 | apprtc.windowPort.messages[1].wsAction);
83 |
84 | assertEquals(WebSocket.CLOSING, this.rws_.readyState);
85 | apprtc.windowPort.simulateMessageFromBackground({
86 | action: Constants.WS_ACTION,
87 | wsAction: Constants.EVENT_ACTION,
88 | wsEvent: Constants.WS_EVENT_ONCLOSE,
89 | data: TEST_MESSAGE
90 | });
91 | assertEquals(true, called);
92 | assertEquals(TEST_MESSAGE, message);
93 | assertEquals(WebSocket.CLOSED, this.rws_.readyState);
94 | };
95 |
96 | RemoteWebSocketTest.prototype.testOnError = function() {
97 | var message = null;
98 | var called = false;
99 | this.rws_.onerror = function(e) {
100 | called = true;
101 | message = e;
102 | };
103 |
104 | apprtc.windowPort.simulateMessageFromBackground({
105 | action: Constants.WS_ACTION,
106 | wsAction: Constants.EVENT_ACTION,
107 | wsEvent: Constants.WS_EVENT_ONERROR,
108 | data: TEST_MESSAGE
109 | });
110 | assertEquals(true, called);
111 | assertEquals(TEST_MESSAGE, message);
112 | };
113 |
114 | RemoteWebSocketTest.prototype.testOnOpen = function() {
115 | var called = false;
116 | this.rws_.onopen = function() {
117 | called = true;
118 | };
119 |
120 | apprtc.windowPort.simulateMessageFromBackground({
121 | action: Constants.WS_ACTION,
122 | wsAction: Constants.EVENT_ACTION,
123 | wsEvent: Constants.WS_EVENT_ONOPEN,
124 | data: TEST_MESSAGE
125 | });
126 | assertEquals(true, called);
127 | assertEquals(WebSocket.OPEN, this.rws_.readyState);
128 | };
129 |
130 | RemoteWebSocketTest.prototype.testOnMessage = function() {
131 | var message = null;
132 | var called = false;
133 | this.rws_.onmessage = function(e) {
134 | called = true;
135 | message = e;
136 | };
137 |
138 | apprtc.windowPort.simulateMessageFromBackground({
139 | action: Constants.WS_ACTION,
140 | wsAction: Constants.EVENT_ACTION,
141 | wsEvent: Constants.WS_EVENT_ONMESSAGE,
142 | data: TEST_MESSAGE
143 | });
144 | assertEquals(true, called);
145 | assertEquals(TEST_MESSAGE, message);
146 | };
147 |
148 | RemoteWebSocketTest.prototype.testOnSendError = function() {
149 | var message = null;
150 | var called = false;
151 | this.rws_.onsenderror = function(e) {
152 | called = true;
153 | message = e;
154 | };
155 |
156 | apprtc.windowPort.simulateMessageFromBackground({
157 | action: Constants.WS_ACTION,
158 | wsAction: Constants.EVENT_ACTION,
159 | wsEvent: Constants.WS_EVENT_SENDERROR,
160 | data: TEST_MESSAGE
161 | });
162 | assertEquals(true, called);
163 | assertEquals(TEST_MESSAGE, message);
164 | };
165 |
--------------------------------------------------------------------------------
/public/js/background.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | // Variables defined in and used from chrome.
12 | /* globals chrome, sendAsyncUrlRequest, Constants, parseJSON */
13 |
14 | 'use strict';
15 | (function() {
16 | chrome.app.runtime.onLaunched.addListener(function() {
17 | chrome.app.window.create('appwindow.html', {
18 | 'width': 800,
19 | 'height': 600,
20 | 'left': 0,
21 | 'top': 0
22 | });
23 | });
24 |
25 | // Send event notification from background to window.
26 | var sendWSEventMessageToWindow = function(port, wsEvent, data) {
27 | var message = {
28 | action: Constants.WS_ACTION,
29 | wsAction: Constants.EVENT_ACTION,
30 | wsEvent: wsEvent
31 | };
32 | if (data) {
33 | message.data = data;
34 | }
35 | trace('B -> W: ' + JSON.stringify(message));
36 | try {
37 | port.postMessage(message);
38 | } catch (ex) {
39 | trace('Error sending message: ' + ex);
40 | }
41 | };
42 |
43 | var handleWebSocketRequest = function(port, message) {
44 | if (message.wsAction === Constants.WS_CREATE_ACTION) {
45 | trace('RWS: creating web socket: ' + message.wssUrl);
46 | var ws = new WebSocket(message.wssUrl);
47 | port.wssPostUrl_ = message.wssPostUrl;
48 | ws.onopen = function() {
49 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_ONOPEN);
50 | };
51 | ws.onerror = function() {
52 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_ONERROR);
53 | };
54 | ws.onclose = function(event) {
55 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_ONCLOSE, event);
56 | };
57 | ws.onmessage = function(event) {
58 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_ONMESSAGE, event);
59 | };
60 | port.webSocket_ = ws;
61 | } else if (message.wsAction === Constants.WS_SEND_ACTION) {
62 | trace('RWS: sending: ' + message.data);
63 | if (port.webSocket_ && port.webSocket_.readyState === WebSocket.OPEN) {
64 | try {
65 | port.webSocket_.send(message.data);
66 | } catch (ex) {
67 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_SENDERROR, ex);
68 | }
69 | } else {
70 | // Attempt to send message using wss port url.
71 | trace('RWS: web socket not ready, falling back to POST.');
72 | var msg = parseJSON(message.data);
73 | if (msg) {
74 | sendAsyncUrlRequest('POST', port.wssPostUrl_, msg.msg);
75 | }
76 | }
77 | } else if (message.wsAction === Constants.WS_CLOSE_ACTION) {
78 | trace('RWS: close');
79 | if (port.webSocket_) {
80 | port.webSocket_.close();
81 | port.webSocket = null;
82 | }
83 | }
84 | };
85 |
86 | var executeCleanupTask = function(port, message) {
87 | trace('Executing queue action: ' + JSON.stringify(message));
88 | if (message.action === Constants.XHR_ACTION) {
89 | var method = message.method;
90 | var url = message.url;
91 | var body = message.body;
92 | return sendAsyncUrlRequest(method, url, body);
93 | } else if (message.action === Constants.WS_ACTION) {
94 | handleWebSocketRequest(port, message);
95 | } else {
96 | trace('Unknown action in cleanup queue: ' + message.action);
97 | }
98 | };
99 |
100 | var executeCleanupTasks = function(port, queue) {
101 | var promise = Promise.resolve();
102 | if (!queue) {
103 | return promise;
104 | }
105 |
106 | var catchFunction = function(error) {
107 | trace('Error executing cleanup action: ' + error.message);
108 | };
109 |
110 | while (queue.length > 0) {
111 | var queueMessage = queue.shift();
112 | promise = promise.then(
113 | executeCleanupTask.bind(null, port, queueMessage)
114 | ).catch(catchFunction);
115 | }
116 | return promise;
117 | };
118 |
119 | var handleMessageFromWindow = function(port, message) {
120 | var action = message.action;
121 | if (action === Constants.WS_ACTION) {
122 | handleWebSocketRequest(port, message);
123 | } else if (action === Constants.QUEUECLEAR_ACTION) {
124 | port.queue_ = [];
125 | } else if (action === Constants.QUEUEADD_ACTION) {
126 | if (!port.queue_) {
127 | port.queue_ = [];
128 | }
129 | port.queue_.push(message.queueMessage);
130 | } else {
131 | trace('Unknown action from window: ' + action);
132 | }
133 | };
134 |
135 | chrome.runtime.onConnect.addListener(function(port) {
136 | port.onDisconnect.addListener(function() {
137 | // Execute the cleanup queue.
138 | executeCleanupTasks(port, port.queue_).then(function() {
139 | // Close web socket.
140 | if (port.webSocket_) {
141 | trace('Closing web socket.');
142 | port.webSocket_.close();
143 | port.webSocket_ = null;
144 | }
145 | });
146 | });
147 |
148 | port.onMessage.addListener(function(message) {
149 | // Handle message from window to background.
150 | trace('W -> B: ' + JSON.stringify(message));
151 | handleMessageFromWindow(port, message);
152 | });
153 | });
154 | })();
155 |
--------------------------------------------------------------------------------
/public/js/call_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals TestCase, SignalingChannel:true, requestUserMedia:true,
12 | assertEquals, assertTrue, MockWindowPort, FAKE_WSS_POST_URL, FAKE_ROOM_ID,
13 | FAKE_CLIENT_ID, apprtc, Constants, xhrs, MockXMLHttpRequest, assertFalse,
14 | XMLHttpRequest:true */
15 |
16 | 'use strict';
17 |
18 | var FAKE_LEAVE_URL = '/leave/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID;
19 | var MEDIA_STREAM_OBJECT = {value: 'stream'};
20 |
21 | var mockSignalingChannels = [];
22 |
23 | var MockSignalingChannel = function() {
24 | this.isOpen = null;
25 | this.sends = [];
26 | mockSignalingChannels.push(this);
27 | };
28 |
29 | MockSignalingChannel.prototype.open = function() {
30 | this.isOpen = true;
31 | return Promise.resolve();
32 | };
33 |
34 | MockSignalingChannel.prototype.getWssPostUrl = function() {
35 | return FAKE_WSS_POST_URL;
36 | };
37 |
38 | MockSignalingChannel.prototype.send = function(data) {
39 | this.sends.push(data);
40 | };
41 |
42 | MockSignalingChannel.prototype.close = function() {
43 | this.isOpen = false;
44 | };
45 |
46 | var CallTest = new TestCase('CallTest');
47 |
48 | function mockRequestUserMedia() {
49 | return new Promise(function(resolve) {
50 | resolve(MEDIA_STREAM_OBJECT);
51 | });
52 | }
53 |
54 | CallTest.prototype.setUp = function() {
55 | mockSignalingChannels = [];
56 | this.signalingChannelBackup_ = SignalingChannel;
57 | SignalingChannel = MockSignalingChannel;
58 | this.requestUserMediaBackup_ = requestUserMedia;
59 | requestUserMedia = mockRequestUserMedia;
60 |
61 | this.params_ = {
62 | mediaConstraints: {
63 | audio: true, video: true
64 | },
65 | roomId: FAKE_ROOM_ID,
66 | clientId: FAKE_CLIENT_ID
67 | };
68 | };
69 |
70 | CallTest.prototype.tearDown = function() {
71 | SignalingChannel = this.signalingChannelBackup_;
72 | requestUserMedia = this.requestUserMediaBackup_;
73 | };
74 |
75 | CallTest.prototype.testRestartInitializesMedia = function() {
76 | var call = new Call(this.params_);
77 | var mediaStarted = false;
78 | call.onlocalstreamadded = function(stream) {
79 | mediaStarted = true;
80 | assertEquals(MEDIA_STREAM_OBJECT, stream);
81 | };
82 | call.restart();
83 | assertTrue(mediaStarted);
84 | };
85 |
86 | CallTest.prototype.testSetUpCleanupQueue = function() {
87 | var realWindowPort = apprtc.windowPort;
88 | apprtc.windowPort = new MockWindowPort();
89 |
90 | var call = new Call(this.params_);
91 | assertEquals(0, apprtc.windowPort.messages.length);
92 | call.queueCleanupMessages_();
93 | assertEquals(3, apprtc.windowPort.messages.length);
94 |
95 | var verifyXhrMessage = function(message, method, url) {
96 | assertEquals(Constants.QUEUEADD_ACTION, message.action);
97 | assertEquals(Constants.XHR_ACTION, message.queueMessage.action);
98 | assertEquals(method, message.queueMessage.method);
99 | assertEquals(url, message.queueMessage.url);
100 | assertEquals(null, message.queueMessage.body);
101 | };
102 |
103 | verifyXhrMessage(apprtc.windowPort.messages[0], 'POST', FAKE_LEAVE_URL);
104 | verifyXhrMessage(apprtc.windowPort.messages[2], 'DELETE',
105 | FAKE_WSS_POST_URL);
106 |
107 | var message = apprtc.windowPort.messages[1];
108 | assertEquals(Constants.QUEUEADD_ACTION, message.action);
109 | assertEquals(Constants.WS_ACTION, message.queueMessage.action);
110 | assertEquals(Constants.WS_SEND_ACTION, message.queueMessage.wsAction);
111 | var data = JSON.parse(message.queueMessage.data);
112 | assertEquals('send', data.cmd);
113 | var msg = JSON.parse(data.msg);
114 | assertEquals('bye', msg.type);
115 |
116 | apprtc.windowPort = realWindowPort;
117 | };
118 |
119 | CallTest.prototype.testClearCleanupQueue = function() {
120 | var realWindowPort = apprtc.windowPort;
121 | apprtc.windowPort = new MockWindowPort();
122 |
123 | var call = new Call(this.params_);
124 | call.queueCleanupMessages_();
125 | assertEquals(3, apprtc.windowPort.messages.length);
126 |
127 | call.clearCleanupQueue_();
128 | assertEquals(4, apprtc.windowPort.messages.length);
129 | var message = apprtc.windowPort.messages[3];
130 | assertEquals(Constants.QUEUECLEAR_ACTION, message.action);
131 |
132 | apprtc.windowPort = realWindowPort;
133 | };
134 |
135 | CallTest.prototype.testCallHangupSync = function() {
136 | var call = new Call(this.params_);
137 | var stopCalled = false;
138 | var closeCalled = false;
139 | call.localStream_ = {stop: function() {stopCalled = true; }};
140 | call.pcClient_ = {close: function() {closeCalled = true; }};
141 |
142 | assertEquals(0, xhrs.length);
143 | assertEquals(0, mockSignalingChannels[0].sends.length);
144 | assertTrue(mockSignalingChannels[0].isOpen !== false);
145 | var realXMLHttpRequest = XMLHttpRequest;
146 | XMLHttpRequest = MockXMLHttpRequest;
147 |
148 | call.hangup(false);
149 | XMLHttpRequest = realXMLHttpRequest;
150 |
151 | assertEquals(true, stopCalled);
152 | assertEquals(true, closeCalled);
153 | // Send /leave.
154 | assertEquals(1, xhrs.length);
155 | assertEquals(FAKE_LEAVE_URL, xhrs[0].url);
156 | assertEquals('POST', xhrs[0].method);
157 |
158 | assertEquals(1, mockSignalingChannels.length);
159 | // Send 'bye' to ws.
160 | assertEquals(1, mockSignalingChannels[0].sends.length);
161 | assertEquals(JSON.stringify({type: 'bye'}),
162 | mockSignalingChannels[0].sends[0]);
163 |
164 | // Close ws.
165 | assertFalse(mockSignalingChannels[0].isOpen);
166 |
167 | // Clean up params state.
168 | assertEquals(null, call.params_.roomId);
169 | assertEquals(null, call.params_.clientId);
170 | assertEquals(FAKE_ROOM_ID, call.params_.previousRoomId);
171 | };
172 |
--------------------------------------------------------------------------------
/public/js/signalingchannel_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals TestCase, assertEquals, assertNotNull, assertTrue, assertFalse,
12 | WebSocket:true, XMLHttpRequest:true, SignalingChannel, webSockets:true,
13 | xhrs:true, FAKE_WSS_URL, FAKE_WSS_POST_URL, FAKE_ROOM_ID, FAKE_CLIENT_ID,
14 | MockXMLHttpRequest, MockWebSocket */
15 |
16 | 'use strict';
17 |
18 | var SignalingChannelTest = new TestCase('SignalingChannelTest');
19 |
20 | SignalingChannelTest.prototype.setUp = function() {
21 | webSockets = [];
22 | xhrs = [];
23 |
24 | this.realWebSocket = WebSocket;
25 | WebSocket = MockWebSocket;
26 |
27 | this.channel =
28 | new SignalingChannel(FAKE_WSS_URL, FAKE_WSS_POST_URL);
29 | };
30 |
31 | SignalingChannelTest.prototype.tearDown = function() {
32 | WebSocket = this.realWebSocket;
33 | };
34 |
35 | SignalingChannelTest.prototype.testOpenSuccess = function() {
36 | var promise = this.channel.open();
37 | assertEquals(1, webSockets.length);
38 |
39 | var resolved = false;
40 | var rejected = false;
41 | promise.then(function() {
42 | resolved = true;
43 | }).catch (function() {
44 | rejected = true;
45 | });
46 |
47 | var socket = webSockets[0];
48 | socket.simulateOpenResult(true);
49 | assertTrue(resolved);
50 | assertFalse(rejected);
51 | };
52 |
53 | SignalingChannelTest.prototype.testReceiveMessage = function() {
54 | this.channel.open();
55 | var socket = webSockets[0];
56 | socket.simulateOpenResult(true);
57 |
58 | assertNotNull(socket.onmessage);
59 |
60 | var msgs = [];
61 | this.channel.onmessage = function(msg) {
62 | msgs.push(msg);
63 | };
64 |
65 | var expectedMsg = 'hi';
66 | var event = {
67 | 'data': JSON.stringify({'msg': expectedMsg})
68 | };
69 | socket.onmessage(event);
70 | assertEquals(1, msgs.length);
71 | assertEquals(expectedMsg, msgs[0]);
72 | };
73 |
74 | SignalingChannelTest.prototype.testOpenFailure = function() {
75 | var promise = this.channel.open();
76 | assertEquals(1, webSockets.length);
77 |
78 | var resolved = false;
79 | var rejected = false;
80 | promise.then(function() {
81 | resolved = true;
82 | }).catch (function() {
83 | rejected = true;
84 | });
85 |
86 | var socket = webSockets[0];
87 | socket.simulateOpenResult(false);
88 | assertFalse(resolved);
89 | assertTrue(rejected);
90 | };
91 |
92 | SignalingChannelTest.prototype.testRegisterBeforeOpen = function() {
93 | this.channel.open();
94 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID);
95 |
96 | var socket = webSockets[0];
97 | socket.simulateOpenResult(true);
98 |
99 | assertEquals(1, socket.messages.length);
100 |
101 | var registerMessage = {
102 | cmd: 'register',
103 | roomid: FAKE_ROOM_ID,
104 | clientid: FAKE_CLIENT_ID
105 | };
106 | assertEquals(JSON.stringify(registerMessage), socket.messages[0]);
107 | };
108 |
109 | SignalingChannelTest.prototype.testRegisterAfterOpen = function() {
110 | this.channel.open();
111 | var socket = webSockets[0];
112 | socket.simulateOpenResult(true);
113 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID);
114 |
115 | assertEquals(1, socket.messages.length);
116 |
117 | var registerMessage = {
118 | cmd: 'register',
119 | roomid: FAKE_ROOM_ID,
120 | clientid: FAKE_CLIENT_ID
121 | };
122 | assertEquals(JSON.stringify(registerMessage), socket.messages[0]);
123 | };
124 |
125 | SignalingChannelTest.prototype.testSendBeforeOpen = function() {
126 | // Stubbing XMLHttpRequest cannot be done in setUp since it caused PhantomJS
127 | // to hang.
128 | var realXMLHttpRequest = XMLHttpRequest;
129 | XMLHttpRequest = MockXMLHttpRequest;
130 |
131 | this.channel.open();
132 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID);
133 |
134 | var message = 'hello';
135 | this.channel.send(message);
136 |
137 | assertEquals(1, xhrs.length);
138 | assertEquals(2, xhrs[0].readyState);
139 | assertEquals(FAKE_WSS_POST_URL + '/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID,
140 | xhrs[0].url);
141 | assertEquals('POST', xhrs[0].method);
142 | assertEquals(message, xhrs[0].body);
143 |
144 | XMLHttpRequest = realXMLHttpRequest;
145 | };
146 |
147 | SignalingChannelTest.prototype.testSendAfterOpen = function() {
148 | this.channel.open();
149 | var socket = webSockets[0];
150 | socket.simulateOpenResult(true);
151 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID);
152 |
153 | var message = 'hello';
154 | var wsMessage = {
155 | cmd: 'send',
156 | msg: message
157 | };
158 | this.channel.send(message);
159 | assertEquals(2, socket.messages.length);
160 | assertEquals(JSON.stringify(wsMessage), socket.messages[1]);
161 | };
162 |
163 | SignalingChannelTest.prototype.testCloseAfterRegister = function() {
164 | var realXMLHttpRequest = XMLHttpRequest;
165 | XMLHttpRequest = MockXMLHttpRequest;
166 |
167 | this.channel.open();
168 | var socket = webSockets[0];
169 | socket.simulateOpenResult(true);
170 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID);
171 |
172 | assertEquals(WebSocket.OPEN, socket.readyState);
173 | this.channel.close();
174 | assertEquals(WebSocket.CLOSED, socket.readyState);
175 |
176 | assertEquals(1, xhrs.length);
177 | assertEquals(4, xhrs[0].readyState);
178 | assertEquals(FAKE_WSS_POST_URL + '/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID,
179 | xhrs[0].url);
180 | assertEquals('DELETE', xhrs[0].method);
181 |
182 | XMLHttpRequest = realXMLHttpRequest;
183 | };
184 |
185 | SignalingChannelTest.prototype.testCloseBeforeRegister = function() {
186 | var realXMLHttpRequest = XMLHttpRequest;
187 | XMLHttpRequest = MockXMLHttpRequest;
188 |
189 | this.channel.open();
190 | this.channel.close();
191 |
192 | assertEquals(0, xhrs.length);
193 | XMLHttpRequest = realXMLHttpRequest;
194 | };
195 |
--------------------------------------------------------------------------------
/public/js/util.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* exported setUpFullScreen, fullScreenElement, isFullScreen,
12 | requestTurnServers, sendAsyncUrlRequest, sendSyncUrlRequest, randomString, $,
13 | queryStringToDictionary */
14 | /* globals chrome */
15 |
16 | 'use strict';
17 |
18 | function $(selector) {
19 | return document.querySelector(selector);
20 | }
21 |
22 | // Returns the URL query key-value pairs as a dictionary object.
23 | function queryStringToDictionary(queryString) {
24 | var pairs = queryString.slice(1).split('&');
25 |
26 | var result = {};
27 | pairs.forEach(function(pair) {
28 | if (pair) {
29 | pair = pair.split('=');
30 | if (pair[0]) {
31 | result[pair[0]] = decodeURIComponent(pair[1] || '');
32 | }
33 | }
34 | });
35 | return result;
36 | }
37 |
38 | // Sends the URL request and returns a Promise as the result.
39 | function sendAsyncUrlRequest(method, url, body) {
40 | return sendUrlRequest(method, url, true, body);
41 | }
42 |
43 | // If async is true, returns a Promise and executes the xhr request
44 | // async. If async is false, the xhr will be executed sync and a
45 | // resolved promise is returned.
46 | function sendUrlRequest(method, url, async, body) {
47 | return new Promise(function(resolve, reject) {
48 | var xhr;
49 | var reportResults = function() {
50 | if (xhr.status !== 200) {
51 | reject(
52 | Error('Status=' + xhr.status + ', response=' +
53 | xhr.responseText));
54 | return;
55 | }
56 | resolve(xhr.responseText);
57 | };
58 |
59 | xhr = new XMLHttpRequest();
60 | if (async) {
61 | xhr.onreadystatechange = function() {
62 | if (xhr.readyState !== 4) {
63 | return;
64 | }
65 | reportResults();
66 | };
67 | }
68 | xhr.open(method, url, async);
69 | xhr.send(body);
70 |
71 | if (!async) {
72 | reportResults();
73 | }
74 | });
75 | }
76 |
77 | // Returns a list of turn servers after requesting it from CEOD.
78 | function requestTurnServers(turnRequestUrl, turnTransports) {
79 | return new Promise(function(resolve, reject) {
80 | // Chrome apps don't send origin header for GET requests, but
81 | // do send it for POST requests. Origin header is required for
82 | // access to turn request url.
83 | var method = isChromeApp() ? 'POST' : 'GET';
84 | sendAsyncUrlRequest(method, turnRequestUrl).then(function(response) {
85 | var turnServerResponse = parseJSON(response);
86 | if (!turnServerResponse) {
87 | reject(Error('Error parsing response JSON: ' + response));
88 | return;
89 | }
90 | // Filter the TURN URLs to only use the desired transport, if specified.
91 | if (turnTransports.length > 0) {
92 | filterTurnUrls(turnServerResponse.uris, turnTransports);
93 | }
94 |
95 | // Create the RTCIceServer objects from the response.
96 | var turnServers = createIceServers(turnServerResponse.uris,
97 | turnServerResponse.username, turnServerResponse.password);
98 | if (!turnServers) {
99 | reject(Error('Error creating ICE servers from response.'));
100 | return;
101 | }
102 | trace('Retrieved TURN server information.');
103 | resolve(turnServers);
104 | }).catch(function(error) {
105 | reject(Error('TURN server request error: ' + error.message));
106 | return;
107 | });
108 | });
109 | }
110 |
111 | // Parse the supplied JSON, or return null if parsing fails.
112 | function parseJSON(json) {
113 | try {
114 | return JSON.parse(json);
115 | } catch (e) {
116 | trace('Error parsing json: ' + json);
117 | }
118 | return null;
119 | }
120 |
121 | // Filter a list of TURN urls to only contain those with transport=|protocol|.
122 | function filterTurnUrls(urls, protocol) {
123 | for (var i = 0; i < urls.length;) {
124 | var parts = urls[i].split('?');
125 | if (parts.length > 1 && parts[1] !== ('transport=' + protocol)) {
126 | urls.splice(i, 1);
127 | } else {
128 | ++i;
129 | }
130 | }
131 | }
132 |
133 | // Start shims for fullscreen
134 | function setUpFullScreen() {
135 | if (isChromeApp()) {
136 | document.cancelFullScreen = function() {
137 | chrome.app.window.current().restore();
138 | };
139 | } else {
140 | document.cancelFullScreen = document.webkitCancelFullScreen ||
141 | document.mozCancelFullScreen || document.cancelFullScreen;
142 | }
143 |
144 | if (isChromeApp()) {
145 | document.body.requestFullScreen = function() {
146 | chrome.app.window.current().fullscreen();
147 | };
148 | } else {
149 | document.body.requestFullScreen = document.body.webkitRequestFullScreen ||
150 | document.body.mozRequestFullScreen || document.body.requestFullScreen;
151 | }
152 |
153 | document.onfullscreenchange = document.onfullscreenchange ||
154 | document.onwebkitfullscreenchange || document.onmozfullscreenchange;
155 | }
156 |
157 | function isFullScreen() {
158 | if (isChromeApp()) {
159 | return chrome.app.window.current().isFullscreen();
160 | }
161 |
162 | return !!(document.webkitIsFullScreen || document.mozFullScreen ||
163 | document.isFullScreen); // if any defined and true
164 | }
165 |
166 | function fullScreenElement() {
167 | return document.webkitFullScreenElement ||
168 | document.webkitCurrentFullScreenElement ||
169 | document.mozFullScreenElement ||
170 | document.fullScreenElement;
171 | }
172 |
173 | // End shims for fullscreen
174 |
175 | // Return a random numerical string.
176 | function randomString(strLength) {
177 | var result = [];
178 | strLength = strLength || 5;
179 | var charSet = '0123456789';
180 | while (strLength--) {
181 | result.push(charSet.charAt(Math.floor(Math.random() * charSet.length)));
182 | }
183 | return result.join('');
184 | }
185 |
186 | // Returns true if the code is running in a packaged Chrome App.
187 | function isChromeApp() {
188 | return (typeof chrome !== 'undefined' &&
189 | typeof chrome.storage !== 'undefined' &&
190 | typeof chrome.storage.local !== 'undefined');
191 | }
192 |
--------------------------------------------------------------------------------
/public/js/roomselection.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals randomString, Storage, parseJSON */
12 | /* exported RoomSelection */
13 |
14 | 'use strict';
15 |
16 | var RoomSelection = function(roomSelectionDiv,
17 | uiConstants, recentRoomsKey, setupCompletedCallback) {
18 | this.roomSelectionDiv_ = roomSelectionDiv;
19 |
20 | this.setupCompletedCallback_ = setupCompletedCallback;
21 |
22 | this.roomIdInput_ = this.roomSelectionDiv_.querySelector(
23 | uiConstants.roomSelectionInput);
24 | this.roomIdInputLabel_ = this.roomSelectionDiv_.querySelector(
25 | uiConstants.roomSelectionInputLabel);
26 | this.roomJoinButton_ = this.roomSelectionDiv_.querySelector(
27 | uiConstants.roomSelectionJoinButton);
28 | this.roomRandomButton_ = this.roomSelectionDiv_.querySelector(
29 | uiConstants.roomSelectionRandomButton);
30 | this.roomRecentList_ = this.roomSelectionDiv_.querySelector(
31 | uiConstants.roomSelectionRecentList);
32 |
33 | this.roomIdInput_.value = randomString(9);
34 | // Call onRoomIdInput_ now to validate initial state of input box.
35 | this.onRoomIdInput_();
36 | this.roomIdInput_.addEventListener('input',
37 | this.onRoomIdInput_.bind(this), false);
38 | this.roomIdInput_.addEventListener('keyup',
39 | this.onRoomIdKeyPress_.bind(this), false);
40 | this.roomRandomButton_.addEventListener('click',
41 | this.onRandomButton_.bind(this), false);
42 | this.roomJoinButton_.addEventListener('click',
43 | this.onJoinButton_.bind(this), false);
44 |
45 | // Public callbacks. Keep it sorted.
46 | this.onRoomSelected = null;
47 |
48 | this.recentlyUsedList_ = new RoomSelection.RecentlyUsedList(recentRoomsKey);
49 | this.startBuildingRecentRoomList_();
50 | };
51 |
52 | RoomSelection.matchRandomRoomPattern = function(input) {
53 | return input.match(/^\d{9}$/) !== null;
54 | };
55 |
56 | RoomSelection.prototype.startBuildingRecentRoomList_ = function() {
57 | this.recentlyUsedList_.getRecentRooms().then(function(recentRooms) {
58 | this.buildRecentRoomList_(recentRooms);
59 | if (this.setupCompletedCallback_) {
60 | this.setupCompletedCallback_();
61 | }
62 | }.bind(this)).catch(function(error) {
63 | trace('Error building recent rooms list: ' + error.message);
64 | }.bind(this));
65 | };
66 |
67 | RoomSelection.prototype.buildRecentRoomList_ = function(recentRooms) {
68 | var lastChild = this.roomRecentList_.lastChild;
69 | while (lastChild) {
70 | this.roomRecentList_.removeChild(lastChild);
71 | lastChild = this.roomRecentList_.lastChild;
72 | }
73 |
74 | for (var i = 0; i < recentRooms.length; ++i) {
75 | // Create link in recent list
76 | var li = document.createElement('li');
77 | var href = document.createElement('a');
78 | var linkText = document.createTextNode(recentRooms[i]);
79 | href.appendChild(linkText);
80 | href.href = location.origin + '/r/' + encodeURIComponent(recentRooms[i]);
81 | li.appendChild(href);
82 | this.roomRecentList_.appendChild(li);
83 |
84 | // Set up click handler to avoid browser navigation.
85 | href.addEventListener('click',
86 | this.makeRecentlyUsedClickHandler_(recentRooms[i]).bind(this), false);
87 | }
88 | };
89 |
90 | RoomSelection.prototype.onRoomIdInput_ = function() {
91 | // Validate room id, enable/disable join button.
92 | // The server currently accepts only the \w character class.
93 | var room = this.roomIdInput_.value;
94 | var valid = room.length >= 5;
95 | var re = /^\w+$/;
96 | valid = valid && re.exec(room);
97 | if (valid) {
98 | this.roomJoinButton_.disabled = false;
99 | this.roomIdInput_.classList.remove('invalid');
100 | this.roomIdInputLabel_.classList.add('hidden');
101 | } else {
102 | this.roomJoinButton_.disabled = true;
103 | this.roomIdInput_.classList.add('invalid');
104 | this.roomIdInputLabel_.classList.remove('hidden');
105 | }
106 | };
107 |
108 | RoomSelection.prototype.onRoomIdKeyPress_ = function(event) {
109 | if (event.which !== 13 || this.roomJoinButton_.disabled) {
110 | return;
111 | }
112 | this.onJoinButton_();
113 | };
114 |
115 | RoomSelection.prototype.onRandomButton_ = function() {
116 | this.roomIdInput_.value = randomString(9);
117 | this.onRoomIdInput_();
118 | };
119 |
120 | RoomSelection.prototype.onJoinButton_ = function() {
121 | this.loadRoom_(this.roomIdInput_.value);
122 | };
123 |
124 | RoomSelection.prototype.makeRecentlyUsedClickHandler_ = function(roomName) {
125 | return function(e) {
126 | e.preventDefault();
127 | this.loadRoom_(roomName);
128 | };
129 | };
130 |
131 | RoomSelection.prototype.loadRoom_ = function(roomName) {
132 | this.recentlyUsedList_.pushRecentRoom(roomName);
133 | if (this.onRoomSelected) {
134 | this.onRoomSelected(roomName);
135 | }
136 | };
137 |
138 | RoomSelection.RecentlyUsedList = function(key) {
139 | // This is the length of the most recently used list.
140 | this.LISTLENGTH_ = 10;
141 |
142 | this.RECENTROOMSKEY_ = key || 'recentRooms';
143 | this.storage_ = new Storage();
144 | };
145 |
146 | // Add a room to the recently used list and store to local storage.
147 | RoomSelection.RecentlyUsedList.prototype.pushRecentRoom = function(roomId) {
148 | // Push recent room to top of recent list, keep max of this.LISTLENGTH_ entries.
149 | return new Promise(function(resolve, reject) {
150 | if (!roomId) {
151 | resolve();
152 | return;
153 | }
154 |
155 | this.getRecentRooms().then(function(recentRooms) {
156 | recentRooms = [roomId].concat(recentRooms);
157 | // Remove any duplicates from the list, leaving the first occurance.
158 | recentRooms = recentRooms.filter(function(value, index, self) {
159 | return self.indexOf(value) === index;
160 | });
161 | recentRooms = recentRooms.slice(0, this.LISTLENGTH_);
162 | this.storage_.setStorage(this.RECENTROOMSKEY_,
163 | JSON.stringify(recentRooms), function() {
164 | resolve();
165 | });
166 | }.bind(this)).catch(function(err) {
167 | reject(err);
168 | }.bind(this));
169 | }.bind(this));
170 | };
171 |
172 | // Get the list of recently used rooms from local storage.
173 | RoomSelection.RecentlyUsedList.prototype.getRecentRooms = function() {
174 | return new Promise(function(resolve) {
175 | this.storage_.getStorage(this.RECENTROOMSKEY_, function(value) {
176 | var recentRooms = parseJSON(value);
177 | if (!recentRooms) {
178 | recentRooms = [];
179 | }
180 | resolve(recentRooms);
181 | });
182 | }.bind(this));
183 | };
184 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/public/html/index_template.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | //
3 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
4 | *
5 | * Use of this source code is governed by a BSD-style license
6 | * that can be found in the LICENSE file in the root of the source
7 | * tree.
8 |
9 | html
10 | head
11 | meta(charset="utf-8")
12 | meta(name="description", content="WebRTC reference app")
13 | meta(name="viewport", content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1")
14 | meta(itemprop="description", content="Video chat using the reference WebRTC application")
15 | meta(itemprop="image", content="/images/webrtc-icon-192x192.png")
16 | meta(itemprop="name", content="AppRTC")
17 | meta(name="mobile-web-app-capable", content="yes")
18 | meta(id="theme-color", name="theme-color", content="#ffffff")
19 |
20 | base(target="_blank")
21 |
22 | title AppRTC
23 |
24 | link(rel="manifest", href="manifest.json")
25 | link(rel="icon", sizes="192x192", href="/images/webrtc-icon-192x192.png")
26 | if !chromeapp
27 | link(rel="canonical", href=room_link)
28 | link(rel="stylesheet", href="/css/main.css")
29 |
30 | body
31 | //
32 | * Keep the HTML id attributes in sync with |UI_CONSTANTS| defined in
33 | * appcontroller.js.
34 | #videos
35 | video#mini-video(autoplay, muted)
36 | video#remote-video(autoplay)
37 | video#local-video(autoplay, muted)
38 |
39 | #room-selection.hidden
40 | h1 AppRTC
41 | p#instructions Please enter a room name.
42 | div
43 | #room-id-input-div
44 | input#room-id-input(type='text', autofocus='')
45 | label#room-id-input-label.error-label.hidden(for='room-id-input') Room name must be 5 or more characters and include only letters and numbers.
46 | #room-id-input-buttons
47 | button#join-button JOIN
48 | button#random-button RANDOM
49 | #recent-rooms
50 | p Recently used rooms:
51 | ul#recent-rooms-list
52 |
53 |
54 | #confirm-join-div.hidden
55 | div Ready to join
56 | span#confirm-join-room-span
57 | | ?
58 | button#confirm-join-button JOIN
59 |
60 | footer
61 | #sharing-div
62 | #room-link Waiting for someone to join this room:
63 | a#room-link-href(href=room_link, target='_blank')= room_link
64 | #info-div
65 | #status-div
66 | #rejoin-div.hidden
67 | span You have left the call.
68 | button#rejoin-button REJOIN
69 | button#new-room-button NEW ROOM
70 |
71 |
72 | #icons.hidden
73 | svg#mute-audio(xmlns='http://www.w3.org/2000/svg', width='48', height='48', viewbox='-10 -10 68 68')
74 | title title
75 | circle(cx='24', cy='24', r='34')
76 | title Mute audio
77 | path.on(transform='scale(0.6), translate(17,18)', d='M38 22h-3.4c0 1.49-.31 2.87-.87 4.1l2.46 2.46C37.33 26.61 38 24.38 38 22zm-8.03.33c0-.11.03-.22.03-.33V10c0-3.32-2.69-6-6-6s-6 2.68-6 6v.37l11.97 11.96zM8.55 6L6 8.55l12.02 12.02v1.44c0 3.31 2.67 6 5.98 6 .45 0 .88-.06 1.3-.15l3.32 3.32c-1.43.66-3 1.03-4.62 1.03-5.52 0-10.6-4.2-10.6-10.2H10c0 6.83 5.44 12.47 12 13.44V42h4v-6.56c1.81-.27 3.53-.9 5.08-1.81L39.45 42 42 39.46 8.55 6z', fill='white')
78 | path.off(transform='scale(0.6), translate(17,18)', d='M24 28c3.31 0 5.98-2.69 5.98-6L30 10c0-3.32-2.68-6-6-6-3.31 0-6 2.68-6 6v12c0 3.31 2.69 6 6 6zm10.6-6c0 6-5.07 10.2-10.6 10.2-5.52 0-10.6-4.2-10.6-10.2H10c0 6.83 5.44 12.47 12 13.44V42h4v-6.56c6.56-.97 12-6.61 12-13.44h-3.4z', fill='white')
79 | svg#mute-video(xmlns='http://www.w3.org/2000/svg', width='48', height='48', viewbox='-10 -10 68 68')
80 | circle(cx='24', cy='24', r='34')
81 | title Mute video
82 | path.on(transform='scale(0.6), translate(17,16)', d='M40 8H15.64l8 8H28v4.36l1.13 1.13L36 16v12.36l7.97 7.97L44 36V12c0-2.21-1.79-4-4-4zM4.55 2L2 4.55l4.01 4.01C4.81 9.24 4 10.52 4 12v24c0 2.21 1.79 4 4 4h29.45l4 4L44 41.46 4.55 2zM12 16h1.45L28 30.55V32H12V16z', fill='white')
83 | path.off(transform='scale(0.6), translate(17,16)', d='M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm-4 24l-8-6.4V32H12V16h16v6.4l8-6.4v16z', fill='white')
84 | svg#fullscreen(xmlns='http://www.w3.org/2000/svg', width='48', height='48', viewbox='-10 -10 68 68')
85 | circle(cx='24', cy='24', r='34')
86 | title Enter fullscreen
87 | path.on(transform='scale(0.8), translate(7,6)', d='M10 32h6v6h4V28H10v4zm6-16h-6v4h10V10h-4v6zm12 22h4v-6h6v-4H28v10zm4-22v-6h-4v10h10v-4h-6z', fill='white')
88 | path.off(transform='scale(0.8), translate(7,6)', d='M14 28h-4v10h10v-4h-6v-6zm-4-8h4v-6h6v-4H10v10zm24 14h-6v4h10V28h-4v6zm-6-24v4h6v6h4V10H28z', fill='white')
89 | svg#hangup.hidden(xmlns='http://www.w3.org/2000/svg', width='48', height='48', viewbox='-10 -10 68 68')
90 | circle(cx='24', cy='24', r='34')
91 | title Hangup
92 | path(transform='scale(0.7), translate(11,10)', d='M24 18c-3.21 0-6.3.5-9.2 1.44v6.21c0 .79-.46 1.47-1.12 1.8-1.95.98-3.74 2.23-5.33 3.7-.36.35-.85.57-1.4.57-.55 0-1.05-.22-1.41-.59L.59 26.18c-.37-.37-.59-.87-.59-1.42 0-.55.22-1.05.59-1.42C6.68 17.55 14.93 14 24 14s17.32 3.55 23.41 9.34c.37.36.59.87.59 1.42 0 .55-.22 1.05-.59 1.41l-4.95 4.95c-.36.36-.86.59-1.41.59-.54 0-1.04-.22-1.4-.57-1.59-1.47-3.38-2.72-5.33-3.7-.66-.33-1.12-1.01-1.12-1.8v-6.21C30.3 18.5 27.21 18 24 18z', fill='white')
93 |
94 |
95 | if !chromeapp
96 | != include_loopback_js
97 |
98 |
99 | script(src='/js/apprtc.debug.js')
100 |
101 | if chromeapp
102 | script(src='/js/appwindow.js')
103 |
104 |
105 | if !chromeapp
106 | script(type="text/javascript").
107 | var loadingParams = {
108 | errorMessages: '!{error_messages}',
109 | isLoopback: #{is_loopback},
110 | mediaConstraints: !{media_constraints},
111 | offerConstraints: !{offer_constraints},
112 | peerConnectionConfig: !{pc_config},
113 | peerConnectionConstraints: !{pc_constraints},
114 | turnRequestUrl: '!{turn_url}',
115 | turnTransports: '!{turn_transports}',
116 | wssUrl: '!{wss_url}',
117 | wssPostUrl: '!{wss_post_url}',
118 | bypassJoinConfirmation: #{bypass_join_confirmation},
119 | versionInfo: '!{version_info}'
120 | };
121 |
122 | var roomId = '!{room_id}';
123 | if (roomId.length > 0) {
124 | loadingParams.roomId = '!{room_id}';
125 | loadingParams.roomLink = '!{room_link}';
126 | }
127 |
128 | var appController;
129 |
130 | function initialize() {
131 | // We don't want to continue if this is triggered from Chrome prerendering,
132 | // since it will register the user to GAE without cleaning it up, causing
133 | // the real navigation to get a "full room" error. Instead we'll initialize
134 | // once the visibility state changes to non-prerender.
135 | if (document.webkitVisibilityState === 'prerender') {
136 | document.addEventListener('webkitvisibilitychange', onVisibilityChange);
137 | return;
138 | }
139 | appController = new AppController(loadingParams);
140 | }
141 |
142 | function onVisibilityChange() {
143 | if (document.webkitVisibilityState === 'prerender') {
144 | return;
145 | }
146 | document.removeEventListener('webkitvisibilitychange', onVisibilityChange);
147 | initialize();
148 | }
149 |
150 | initialize();
151 |
--------------------------------------------------------------------------------
/public/js/background_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals TestCase, assertEquals, xhrs:true, MockWebSocket, FAKE_WSS_POST_URL,
12 | Constants, webSockets:true, FAKE_WSS_URL, WebSocket:true, MockXMLHttpRequest,
13 | XMLHttpRequest:true, FAKE_SEND_EXCEPTION */
14 |
15 | 'use strict';
16 | var FAKE_MESSAGE = JSON.stringify({
17 | cmd: 'send',
18 | msg: JSON.stringify({type: 'bye'})
19 | });
20 |
21 | var MockEvent = function(addListener) {
22 | this.addListener_ = addListener;
23 | };
24 |
25 | MockEvent.prototype.addListener = function(callback) {
26 | this.addListener_(callback);
27 | };
28 |
29 | var MockPort = function() {
30 | this.onDisconnectCallback_ = null;
31 | this.onMessageCallback_ = null;
32 | this.onPostMessage = null;
33 |
34 | this.onDisconnect = new MockEvent(function(callback) {
35 | this.onDisconnectCallback_ = callback;
36 | }.bind(this));
37 |
38 | this.onMessage = new MockEvent(function(callback) {
39 | this.onMessageCallback_ = callback;
40 | }.bind(this));
41 | };
42 |
43 | MockPort.prototype.disconnect = function() {
44 | if (this.onDisconnectCallback_) {
45 | this.onDisconnectCallback_();
46 | }
47 | };
48 |
49 | MockPort.prototype.message = function(message) {
50 | if (this.onMessageCallback_) {
51 | this.onMessageCallback_(message);
52 | }
53 | };
54 |
55 | MockPort.prototype.postMessage = function(message) {
56 | if (this.onPostMessage) {
57 | this.onPostMessage(message);
58 | }
59 | };
60 |
61 | MockPort.prototype.createWebSocket = function() {
62 | assertEquals(0, webSockets.length);
63 |
64 | this.message({
65 | action: Constants.WS_ACTION,
66 | wsAction: Constants.WS_CREATE_ACTION,
67 | wssUrl: FAKE_WSS_URL,
68 | wssPostUrl: FAKE_WSS_POST_URL
69 | });
70 |
71 | assertEquals(1, webSockets.length);
72 |
73 | assertEquals(WebSocket.CONNECTING, this.webSocket_.readyState);
74 | };
75 |
76 | var BackgroundTest = new TestCase('BackgroundTest');
77 |
78 | BackgroundTest.prototype.setUp = function() {
79 | webSockets = [];
80 | xhrs = [];
81 |
82 | this.realWebSocket = WebSocket;
83 | WebSocket = MockWebSocket;
84 |
85 | this.mockPort_ = new MockPort();
86 | window.chrome.callOnConnect(this.mockPort_);
87 | };
88 |
89 | BackgroundTest.prototype.tearDown = function() {
90 | WebSocket = this.realWebSocket;
91 | };
92 |
93 | BackgroundTest.prototype.testCreateWebSocket = function() {
94 | this.mockPort_.createWebSocket();
95 | };
96 |
97 | BackgroundTest.prototype.testCloseWebSocket = function() {
98 | this.mockPort_.createWebSocket();
99 |
100 | this.mockPort_.message({
101 | action: Constants.WS_ACTION,
102 | wsAction: Constants.WS_CLOSE_ACTION
103 | });
104 |
105 | assertEquals(WebSocket.CLOSED, this.mockPort_.webSocket_.readyState);
106 | };
107 |
108 | BackgroundTest.prototype.testSendWebSocket = function() {
109 | this.mockPort_.createWebSocket();
110 |
111 | this.mockPort_.webSocket_.simulateOpenResult(true);
112 |
113 | assertEquals(0, this.mockPort_.webSocket_.messages.length);
114 | this.mockPort_.message({
115 | action: Constants.WS_ACTION,
116 | wsAction: Constants.WS_SEND_ACTION,
117 | data: FAKE_MESSAGE
118 | });
119 | assertEquals(1, this.mockPort_.webSocket_.messages.length);
120 | assertEquals(FAKE_MESSAGE, this.mockPort_.webSocket_.messages[0]);
121 | };
122 |
123 | BackgroundTest.prototype.testSendWebSocketNotReady = function() {
124 | this.mockPort_.createWebSocket();
125 |
126 | // Send without socket being in open state.
127 | assertEquals(0, this.mockPort_.webSocket_.messages.length);
128 | var realXMLHttpRequest = XMLHttpRequest;
129 | XMLHttpRequest = MockXMLHttpRequest;
130 | this.mockPort_.message({
131 | action: Constants.WS_ACTION,
132 | wsAction: Constants.WS_SEND_ACTION,
133 | data: FAKE_MESSAGE
134 | });
135 | XMLHttpRequest = realXMLHttpRequest;
136 | // No messages posted to web socket.
137 | assertEquals(0, this.mockPort_.webSocket_.messages.length);
138 |
139 | // Message sent via xhr instead.
140 | assertEquals(1, xhrs.length);
141 | assertEquals(2, xhrs[0].readyState);
142 | assertEquals(FAKE_WSS_POST_URL, xhrs[0].url);
143 | assertEquals('POST', xhrs[0].method);
144 | assertEquals(JSON.stringify({type: 'bye'}), xhrs[0].body);
145 | };
146 |
147 | BackgroundTest.prototype.testSendWebSocketThrows = function() {
148 | this.mockPort_.createWebSocket();
149 |
150 | this.mockPort_.webSocket_.simulateOpenResult(true);
151 |
152 | // Set mock web socket to throw exception on send().
153 | this.mockPort_.webSocket_.throwOnSend = true;
154 |
155 | var message = null;
156 | this.mockPort_.onPostMessage = function(msg) {
157 | message = msg;
158 | };
159 |
160 | assertEquals(0, this.mockPort_.webSocket_.messages.length);
161 | this.mockPort_.message({
162 | action: Constants.WS_ACTION,
163 | wsAction: Constants.WS_SEND_ACTION,
164 | data: FAKE_MESSAGE
165 | });
166 | assertEquals(0, this.mockPort_.webSocket_.messages.length);
167 |
168 | this.checkMessage_(message,
169 | Constants.WS_EVENT_SENDERROR, FAKE_SEND_EXCEPTION);
170 | };
171 |
172 | BackgroundTest.prototype.checkMessage_ = function(m, wsEvent, data) {
173 | assertEquals(Constants.WS_ACTION, m.action);
174 | assertEquals(Constants.EVENT_ACTION, m.wsAction);
175 | assertEquals(wsEvent, m.wsEvent);
176 | if (data) {
177 | assertEquals(data, m.data);
178 | }
179 | };
180 |
181 | BackgroundTest.prototype.testWebSocketEvents = function() {
182 | this.mockPort_.createWebSocket();
183 | var message = null;
184 | this.mockPort_.onPostMessage = function(msg) {
185 | message = msg;
186 | };
187 |
188 | var ws = this.mockPort_.webSocket_;
189 |
190 | ws.onopen();
191 | this.checkMessage_(message, Constants.WS_EVENT_ONOPEN);
192 |
193 | ws.onerror();
194 | this.checkMessage_(message, Constants.WS_EVENT_ONERROR);
195 |
196 | ws.onclose(FAKE_MESSAGE);
197 | this.checkMessage_(message, Constants.WS_EVENT_ONCLOSE, FAKE_MESSAGE);
198 |
199 | ws.onmessage(FAKE_MESSAGE);
200 | this.checkMessage_(message, Constants.WS_EVENT_ONMESSAGE, FAKE_MESSAGE);
201 | };
202 |
203 | BackgroundTest.prototype.testDisconnectClosesWebSocket = function() {
204 | // Disconnect should cause web socket to be closed.
205 | var socketClosed = false;
206 |
207 | this.mockPort_.webSocket_ = {
208 | close: function() {
209 | socketClosed = true;
210 | }
211 | };
212 | this.mockPort_.disconnect();
213 |
214 | assertEquals(true, socketClosed);
215 | };
216 |
217 | BackgroundTest.prototype.testQueueMessages = function() {
218 | assertEquals(null, this.mockPort_.queue_);
219 |
220 | this.mockPort_.message({
221 | action: Constants.QUEUEADD_ACTION,
222 | queueMessage: {
223 | action: Constants.XHR_ACTION,
224 | method: 'POST',
225 | url: '/go/home',
226 | body: null
227 | }
228 | });
229 |
230 | assertEquals(1, this.mockPort_.queue_.length);
231 |
232 | this.mockPort_.message({
233 | action: Constants.QUEUEADD_ACTION,
234 | queueMessage: {
235 | action: Constants.WS_ACTION,
236 | wsAction: Constants.WS_SEND_ACTION,
237 | data: JSON.stringify({
238 | cmd: 'send',
239 | msg: JSON.stringify({type: 'bye'})
240 | })
241 | }
242 | });
243 |
244 | assertEquals(2, this.mockPort_.queue_.length);
245 |
246 | this.mockPort_.message({action: Constants.QUEUECLEAR_ACTION});
247 | assertEquals([], this.mockPort_.queue_);
248 | };
249 |
--------------------------------------------------------------------------------
/public/js/adapter.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 | /* global mozRTCIceCandidate, mozRTCPeerConnection,
11 | mozRTCSessionDescription, webkitRTCPeerConnection */
12 | /* exported trace,requestUserMedia */
13 |
14 | 'use strict';
15 |
16 | var RTCPeerConnection = null;
17 | var getUserMedia = null;
18 | var attachMediaStream = null;
19 | var reattachMediaStream = null;
20 | var webrtcDetectedBrowser = null;
21 | var webrtcDetectedVersion = null;
22 |
23 | function trace(text) {
24 | // This function is used for logging.
25 | if (text[text.length - 1] === '\n') {
26 | text = text.substring(0, text.length - 1);
27 | }
28 | if (window.performance) {
29 | var now = (window.performance.now() / 1000).toFixed(3);
30 | console.log(now + ': ' + text);
31 | } else {
32 | console.log(text);
33 | }
34 | }
35 |
36 | if (navigator.mozGetUserMedia) {
37 | console.log('This appears to be Firefox');
38 |
39 | webrtcDetectedBrowser = 'firefox';
40 |
41 | webrtcDetectedVersion =
42 | parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
43 |
44 | // The RTCPeerConnection object.
45 | RTCPeerConnection = function(pcConfig, pcConstraints) {
46 | // .urls is not supported in FF yet.
47 | if (pcConfig && pcConfig.iceServers) {
48 | for (var i = 0; i < pcConfig.iceServers.length; i++) {
49 | if (pcConfig.iceServers[i].hasOwnProperty('urls')) {
50 | pcConfig.iceServers[i].url = pcConfig.iceServers[i].urls;
51 | delete pcConfig.iceServers[i].urls;
52 | }
53 | }
54 | }
55 | return new mozRTCPeerConnection(pcConfig, pcConstraints);
56 | };
57 |
58 | // The RTCSessionDescription object.
59 | window.RTCSessionDescription = mozRTCSessionDescription;
60 |
61 | // The RTCIceCandidate object.
62 | window.RTCIceCandidate = mozRTCIceCandidate;
63 |
64 | // getUserMedia shim (only difference is the prefix).
65 | // Code from Adam Barth.
66 | getUserMedia = navigator.mozGetUserMedia.bind(navigator);
67 | navigator.getUserMedia = getUserMedia;
68 |
69 | // Shim for MediaStreamTrack.getSources.
70 | MediaStreamTrack.getSources = function(successCb) {
71 | setTimeout(function() {
72 | var infos = [
73 | {kind: 'audio', id: 'default', label:'', facing:''},
74 | {kind: 'video', id: 'default', label:'', facing:''}
75 | ];
76 | successCb(infos);
77 | }, 0);
78 | };
79 |
80 | // Creates ICE server from the URL for FF.
81 | window.createIceServer = function(url, username, password) {
82 | var iceServer = null;
83 | var urlParts = url.split(':');
84 | if (urlParts[0].indexOf('stun') === 0) {
85 | // Create ICE server with STUN URL.
86 | iceServer = {
87 | 'url': url
88 | };
89 | } else if (urlParts[0].indexOf('turn') === 0) {
90 | if (webrtcDetectedVersion < 27) {
91 | // Create iceServer with turn url.
92 | // Ignore the transport parameter from TURN url for FF version <=27.
93 | var turnUrlParts = url.split('?');
94 | // Return null for createIceServer if transport=tcp.
95 | if (turnUrlParts.length === 1 ||
96 | turnUrlParts[1].indexOf('transport=udp') === 0) {
97 | iceServer = {
98 | 'url': turnUrlParts[0],
99 | 'credential': password,
100 | 'username': username
101 | };
102 | }
103 | } else {
104 | // FF 27 and above supports transport parameters in TURN url,
105 | // So passing in the full url to create iceServer.
106 | iceServer = {
107 | 'url': url,
108 | 'credential': password,
109 | 'username': username
110 | };
111 | }
112 | }
113 | return iceServer;
114 | };
115 |
116 | window.createIceServers = function(urls, username, password) {
117 | var iceServers = [];
118 | // Use .url for FireFox.
119 | for (var i = 0; i < urls.length; i++) {
120 | var iceServer =
121 | window.createIceServer(urls[i], username, password);
122 | if (iceServer !== null) {
123 | iceServers.push(iceServer);
124 | }
125 | }
126 | return iceServers;
127 | };
128 |
129 | // Attach a media stream to an element.
130 | attachMediaStream = function(element, stream) {
131 | console.log('Attaching media stream');
132 | element.mozSrcObject = stream;
133 | };
134 |
135 | reattachMediaStream = function(to, from) {
136 | console.log('Reattaching media stream');
137 | to.mozSrcObject = from.mozSrcObject;
138 | };
139 |
140 | } else if (navigator.webkitGetUserMedia) {
141 | console.log('This appears to be Chrome');
142 |
143 | webrtcDetectedBrowser = 'chrome';
144 | // Temporary fix until crbug/374263 is fixed.
145 | // Setting Chrome version to 999, if version is unavailable.
146 | var result = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
147 | if (result !== null) {
148 | webrtcDetectedVersion = parseInt(result[2], 10);
149 | } else {
150 | webrtcDetectedVersion = 999;
151 | }
152 |
153 | // Creates iceServer from the url for Chrome M33 and earlier.
154 | window.createIceServer = function(url, username, password) {
155 | var iceServer = null;
156 | var urlParts = url.split(':');
157 | if (urlParts[0].indexOf('stun') === 0) {
158 | // Create iceServer with stun url.
159 | iceServer = {
160 | 'url': url
161 | };
162 | } else if (urlParts[0].indexOf('turn') === 0) {
163 | // Chrome M28 & above uses below TURN format.
164 | iceServer = {
165 | 'url': url,
166 | 'credential': password,
167 | 'username': username
168 | };
169 | }
170 | return iceServer;
171 | };
172 |
173 | // Creates an ICEServer object from multiple URLs.
174 | window.createIceServers = function(urls, username, password) {
175 | return {
176 | 'urls': urls,
177 | 'credential': password,
178 | 'username': username
179 | };
180 | };
181 |
182 | // The RTCPeerConnection object.
183 | RTCPeerConnection = function(pcConfig, pcConstraints) {
184 | return new webkitRTCPeerConnection(pcConfig, pcConstraints);
185 | };
186 |
187 | // Get UserMedia (only difference is the prefix).
188 | // Code from Adam Barth.
189 | getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
190 | navigator.getUserMedia = getUserMedia;
191 |
192 | // Attach a media stream to an element.
193 | attachMediaStream = function(element, stream) {
194 | if (typeof element.srcObject !== 'undefined') {
195 | element.srcObject = stream;
196 | } else if (typeof element.mozSrcObject !== 'undefined') {
197 | element.mozSrcObject = stream;
198 | } else if (typeof element.src !== 'undefined') {
199 | element.src = URL.createObjectURL(stream);
200 | } else {
201 | console.log('Error attaching stream to element.');
202 | }
203 | };
204 |
205 | reattachMediaStream = function(to, from) {
206 | to.src = from.src;
207 | };
208 | } else {
209 | console.log('Browser does not appear to be WebRTC-capable');
210 | }
211 |
212 | // Returns the result of getUserMedia as a Promise.
213 | function requestUserMedia(constraints) {
214 | return new Promise(function(resolve, reject) {
215 | var onSuccess = function(stream) {
216 | resolve(stream);
217 | };
218 | var onError = function(error) {
219 | reject(error);
220 | };
221 |
222 | try {
223 | getUserMedia(constraints, onSuccess, onError);
224 | } catch (e) {
225 | reject(e);
226 | }
227 | });
228 | }
229 |
--------------------------------------------------------------------------------
/public/css/main.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | .hidden {
10 | display: none;
11 | }
12 |
13 | a {
14 | color: #4285F4;
15 | text-decoration: none;
16 | }
17 |
18 | a:hover {
19 | color: #3B78E7;
20 | text-decoration: underline;
21 | }
22 |
23 | #room-link a {
24 | white-space: nowrap;
25 | }
26 |
27 | body {
28 | background-color: #333;
29 | font-family: 'Roboto', 'Open Sans', 'Lucida Grande', sans-serif;
30 | height: 100%;
31 | margin: 0;
32 | padding: 0;
33 | width: 100%;
34 | color: #fff;
35 | }
36 |
37 | #remote-canvas {
38 | display: none;
39 | height: 100%;
40 | margin: 0 auto;
41 | width: 100%;
42 | }
43 |
44 | div.warning {
45 | background-color: #a80202;
46 | color: black;
47 | font-weight: 400;
48 | opacity: .9;
49 | }
50 |
51 | #container {
52 | height: 100%;
53 | position: absolute;
54 | }
55 |
56 | #info-div {
57 | z-index: 3;
58 | }
59 |
60 | #room-link {
61 | margin: 0 0 29px 0;
62 | }
63 |
64 | #status {
65 | z-index: 4;
66 | }
67 |
68 | #videos {
69 | font-size: 0; /* to fix whitespace/scrollbars problem */
70 | height: 100%;
71 | pointer-events: none;
72 | position: absolute;
73 | transition: all 1s;
74 | width: 100%;
75 | }
76 |
77 | #videos.active {
78 | -moz-transform: rotateY(180deg);
79 | -ms-transform: rotateY(180deg);
80 | -o-transform: rotateY(180deg);
81 | -webkit-transform: rotateY(180deg);
82 | transform: rotateY(180deg);
83 | }
84 |
85 | footer > div {
86 | background-color: black;
87 | bottom: 0;
88 | color: white;
89 | display: none;
90 | font-size: .9em;
91 | font-weight: 300;
92 | line-height: 2em;
93 | max-height: 80%;
94 | opacity: 0;
95 | overflow-y: auto;
96 | padding: 10px;
97 | position: absolute;
98 | transition: opacity 1s;
99 | width: calc(100% - 20px);
100 | }
101 |
102 | footer > div.active {
103 | display: block;
104 | opacity: .8;
105 | }
106 |
107 | html {
108 | height: 100%;
109 | margin: 0;
110 | width: 100%;
111 | }
112 |
113 | label {
114 | margin: 0 10px 0 0;
115 | }
116 |
117 | #local-video {
118 | height: 100%;
119 | max-height: 100%;
120 | max-width: 100%;
121 | object-fit: cover; /* no letterboxing */
122 | transition: opacity 1s;
123 | -moz-transform: scale(-1, 1);
124 | -ms-transform: scale(-1, 1);
125 | -o-transform: scale(-1, 1);
126 | -webkit-transform: scale(-1, 1);
127 | transform: scale(-1, 1);
128 | width: 100%;
129 | }
130 |
131 | #mini-video {
132 | border: 1px solid gray;
133 | bottom: 20px;
134 | left: 20px;
135 | /* video div is flipped horizontally when active*/
136 | max-height: 17%;
137 | max-width: 17%;
138 | opacity: 0;
139 | position: absolute;
140 | transition: opacity 1s;
141 | }
142 |
143 | #mini-video.active {
144 | opacity: 1;
145 | z-index: 2;
146 | }
147 |
148 | #remote-video {
149 | display: block;
150 | height: 100%;
151 | max-height: 100%;
152 | max-width: 100%;
153 | object-fit: cover; /* no letterboxing */
154 | opacity: 0;
155 | position: absolute;
156 | -moz-transform: rotateY(180deg);
157 | -ms-transform: rotateY(180deg);
158 | -o-transform: rotateY(180deg);
159 | -webkit-transform: rotateY(180deg);
160 | transform: rotateY(180deg);
161 | transition: opacity 1s;
162 | width: 100%;
163 | }
164 |
165 | #remote-video.active {
166 | opacity: 1;
167 | z-index: 1;
168 | }
169 |
170 | #confirm-join-div {
171 | z-index: 5;
172 | position: absolute;
173 | top: 80%;
174 | width: 100%;
175 | text-align: center;
176 | }
177 |
178 | #confirm-join-div div {
179 | margin-bottom: 10px;
180 | font-size: 1.5em;
181 | }
182 |
183 | /*////// room selection start ///////////////////*/
184 | #recent-rooms-list {
185 | list-style-type: none;
186 | padding: 0 15px;
187 | }
188 |
189 | button {
190 | background-color: #4285F4;
191 | border: none;
192 | border-radius: 2px;
193 | color: white;
194 | font-size: 0.8em;
195 | margin: 0 5px 20px 5px;
196 | width: 8em;
197 | height: 2.75em;
198 | padding: 0.5em 0.7em 0.5em 0.7em;
199 | -webkit-box-shadow: 1px 1px 5px 0 rgba(0,0,0,.5);
200 | -moz-box-shadow: 1px 1px 5px 0 rgba(0,0,0,.5);
201 | box-shadow: 1px 1px 5px 0 rgba(0,0,0,.5);
202 | }
203 |
204 | button:active {
205 | background-color: #3367D6;
206 | }
207 |
208 | button:hover {
209 | background-color: #3B78E7;
210 | }
211 |
212 | button:focus {
213 | outline: none;
214 | -webkit-box-shadow: 0 10px 15px 0 rgba(0,0,0,.5);
215 | -moz-box-shadow: 0 10px 15px 0 rgba(0,0,0,.5);
216 | box-shadow: 0 10px 15px 0 rgba(0,0,0,.5);
217 | }
218 |
219 | button[disabled] {
220 | color: rgb(76, 76, 76);
221 | color: rgba(255, 255, 255, 0.3);
222 | background-color: rgb(30, 30, 30);
223 | background-color: rgba(255, 255, 255, 0.12);
224 | }
225 |
226 | input[type=text] {
227 | border: none;
228 | border-bottom: solid 1px #4c4c4f;
229 | font-size: 1em;
230 | background-color: transparent;
231 | color: #fff;
232 | padding:.4em 0;
233 | margin-right: 20px;
234 | width: 100%;
235 | display: block;
236 | }
237 |
238 | input[type="text"]:focus {
239 | border-bottom: solid 2px #4285F4;
240 | outline: none;
241 | }
242 |
243 | input[type="text"].invalid {
244 | border-bottom: solid 2px #F44336;
245 | }
246 |
247 | label.error-label {
248 | color: #F44336;
249 | font-size: .85em;
250 | font-weight: 200;
251 | margin: 0;
252 | }
253 |
254 | #room-id-input-div {
255 | margin: 15px;
256 | }
257 |
258 | #room-id-input-buttons {
259 | margin: 15px;
260 | }
261 |
262 | h1 {
263 | font-weight: 300;
264 | margin: 0 0 0.8em 0;
265 | padding: 0 0 0.2em 0;
266 | }
267 |
268 | div#room-selection {
269 | margin: 3em auto 0 auto;
270 | width: 25em;
271 | padding: 1em 1.5em 1.3em 1.5em;
272 | }
273 |
274 | p {
275 | color: #eee;
276 | font-weight: 300;
277 | line-height: 1.6em;
278 | }
279 |
280 | /*////// room selection end /////////////////////*/
281 |
282 | /*////// icons CSS start ////////////////////////*/
283 |
284 | #icons {
285 | bottom: 77px;
286 | left: 6vw;
287 | position: absolute;
288 | }
289 |
290 | circle {
291 | fill: #666;
292 | fill-opacity: 0.6;
293 | }
294 |
295 | svg.on circle {
296 | fill-opacity: 0;
297 | }
298 |
299 | /* on icons are hidden by default */
300 | path.on {
301 | display: none;
302 | }
303 |
304 | /* off icons are displayed by default */
305 | path.off {
306 | display: block;
307 | }
308 |
309 | /* on icons are displayed when parent svg has class 'on' */
310 | svg.on path.on {
311 | display: block;
312 | }
313 |
314 | /* off icons are hidden when parent svg has class 'on' */
315 | svg.on path.off {
316 | display: none;
317 | }
318 |
319 | svg {
320 | box-shadow: 2px 2px 24px #444;
321 | border-radius: 48px;
322 | display: block;
323 | margin: 0 0 3vh 0;
324 | transform: translateX(calc(-6vw - 96px));
325 | transition: all .1s;
326 | transition-timing-function: ease-in-out;
327 | }
328 |
329 | svg:hover {
330 | box-shadow: 4px 4px 48px #666;
331 | }
332 |
333 | #icons.active svg {
334 | transform: translateX(0);
335 | }
336 |
337 | #mute-audio {
338 | transition: 40ms;
339 | }
340 |
341 | #mute-audio:hover,
342 | #mute-audio.on {
343 | background: #ab47bc;
344 | }
345 |
346 | #mute-audio:hover circle {
347 | fill: #ab47bc;
348 | }
349 |
350 | #mute-video {
351 | transition: 120ms;
352 | }
353 |
354 | #mute-video:hover,
355 | #mute-video.on {
356 | background: #ff9f00;
357 | }
358 |
359 | #mute-video:hover circle {
360 | fill: #ff9f00;
361 | }
362 |
363 | #switch-video {
364 | transition: 200ms;
365 | }
366 |
367 | #switch-video:hover {
368 | background: #12a256;
369 | }
370 |
371 | #switch-video:hover circle {
372 | fill: #12a256;
373 | }
374 |
375 | #fullscreen {
376 | transition: 280ms;
377 | }
378 |
379 | #fullscreen:hover,
380 | #fullscreen.on {
381 | background: #407cf7;
382 | }
383 |
384 | #fullscreen:hover circle {
385 | fill: #407cf7;
386 | }
387 |
388 | #hangup {
389 | transition: 360ms;
390 | }
391 |
392 | #hangup:hover {
393 | background: #dd2c00;
394 | }
395 | #hangup:hover circle {
396 | fill: #dd2c00;
397 | }
398 |
399 | /*////// icons CSS end /////////////////////////*/
400 |
401 |
--------------------------------------------------------------------------------
/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/public/js/infobox.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals computeBitrate, computeE2EDelay, extractStatAsInt, getStatsReport,
12 | iceCandidateType, computeRate */
13 | /* exported InfoBox */
14 |
15 | 'use strict';
16 |
17 | var InfoBox = function(infoDiv, remoteVideo, call, versionInfo) {
18 | this.infoDiv_ = infoDiv;
19 | this.remoteVideo_ = remoteVideo;
20 | this.call_ = call;
21 | this.versionInfo_ = versionInfo;
22 |
23 | this.errorMessages_ = [];
24 | // Time when the call was intiated and accepted.
25 | this.startTime_ = null;
26 | this.connectTime_ = null;
27 | this.stats_ = null;
28 | this.prevStats_ = null;
29 | this.getStatsTimer_ = null;
30 |
31 | // Types of gathered ICE Candidates.
32 | this.iceCandidateTypes_ = {
33 | Local: {},
34 | Remote: {}
35 | };
36 | };
37 |
38 | InfoBox.prototype.recordIceCandidateTypes = function(location, candidate) {
39 | var type = iceCandidateType(candidate);
40 |
41 | var types = this.iceCandidateTypes_[location];
42 | if (!types[type]) {
43 | types[type] = 1;
44 | } else {
45 | ++types[type];
46 | }
47 | this.updateInfoDiv();
48 | };
49 |
50 | InfoBox.prototype.pushErrorMessage = function(msg) {
51 | this.errorMessages_.push(msg);
52 | this.updateInfoDiv();
53 | this.showInfoDiv();
54 | };
55 |
56 | InfoBox.prototype.setSetupTimes = function(startTime, connectTime) {
57 | this.startTime_ = startTime;
58 | this.connectTime_ = connectTime;
59 | };
60 |
61 | InfoBox.prototype.showInfoDiv = function() {
62 | this.getStatsTimer_ = setInterval(this.refreshStats_.bind(this), 1000);
63 | this.refreshStats_();
64 | this.infoDiv_.classList.add('active');
65 | };
66 |
67 | InfoBox.prototype.toggleInfoDiv = function() {
68 | if (this.infoDiv_.classList.contains('active')) {
69 | clearInterval(this.getStatsTimer_);
70 | this.infoDiv_.classList.remove('active');
71 | } else {
72 | this.showInfoDiv();
73 | }
74 | };
75 |
76 | InfoBox.prototype.refreshStats_ = function() {
77 | this.call_.getPeerConnectionStats(function(response) {
78 | this.prevStats_ = this.stats_;
79 | this.stats_ = response.result();
80 | this.updateInfoDiv();
81 | }.bind(this));
82 | };
83 |
84 | InfoBox.prototype.updateInfoDiv = function() {
85 | var contents = '';
86 |
87 | if (this.stats_) {
88 | var states = this.call_.getPeerConnectionStates();
89 | if (!states) {
90 | return;
91 | }
92 | // Build the display.
93 | contents += this.buildLine_('States');
94 | contents += this.buildLine_('Signaling', states.signalingState);
95 | contents += this.buildLine_('Gathering', states.iceGatheringState);
96 | contents += this.buildLine_('Connection', states.iceConnectionState);
97 | for (var endpoint in this.iceCandidateTypes_) {
98 | var types = [];
99 | for (var type in this.iceCandidateTypes_[endpoint]) {
100 | types.push(type + ':' + this.iceCandidateTypes_[endpoint][type]);
101 | }
102 | contents += this.buildLine_(endpoint, types.join(' '));
103 | }
104 |
105 | var activeCandPair = getStatsReport(this.stats_, 'googCandidatePair',
106 | 'googActiveConnection', 'true');
107 | var localAddr;
108 | var remoteAddr;
109 | var localAddrType;
110 | var remoteAddrType;
111 | if (activeCandPair) {
112 | localAddr = activeCandPair.stat('googLocalAddress');
113 | remoteAddr = activeCandPair.stat('googRemoteAddress');
114 | localAddrType = activeCandPair.stat('googLocalCandidateType');
115 | remoteAddrType = activeCandPair.stat('googRemoteCandidateType');
116 | }
117 | if (localAddr && remoteAddr) {
118 | contents += this.buildLine_('LocalAddr', localAddr +
119 | ' (' + localAddrType + ')');
120 | contents += this.buildLine_('RemoteAddr', remoteAddr +
121 | ' (' + remoteAddrType + ')');
122 | }
123 | contents += this.buildLine_();
124 |
125 | contents += this.buildStatsSection_();
126 | }
127 |
128 | if (this.errorMessages_.length) {
129 | this.infoDiv_.classList.add('warning');
130 | for (var i = 0; i !== this.errorMessages_.length; ++i) {
131 | contents += this.errorMessages_[i] + '\n';
132 | }
133 | } else {
134 | this.infoDiv_.classList.remove('warning');
135 | }
136 |
137 | if (this.versionInfo_) {
138 | contents += this.buildLine_();
139 | contents += this.buildLine_('Version');
140 | for (var key in this.versionInfo_) {
141 | contents += this.buildLine_(key, this.versionInfo_[key]);
142 | }
143 | }
144 |
145 | contents += '';
146 |
147 | if (this.infoDiv_.innerHTML !== contents) {
148 | this.infoDiv_.innerHTML = contents;
149 | }
150 | };
151 |
152 | InfoBox.prototype.buildStatsSection_ = function() {
153 | var contents = this.buildLine_('Stats');
154 |
155 | // Obtain setup and latency this.stats_.
156 | var rtt = extractStatAsInt(this.stats_, 'ssrc', 'googRtt');
157 | var captureStart = extractStatAsInt(this.stats_, 'ssrc',
158 | 'googCaptureStartNtpTimeMs');
159 | var e2eDelay = computeE2EDelay(captureStart, this.remoteVideo_.currentTime);
160 | if (this.endTime_ !== null) {
161 | contents += this.buildLine_('Call time',
162 | InfoBox.formatInterval_(window.performance.now() - this.connectTime_));
163 | contents += this.buildLine_('Setup time',
164 | InfoBox.formatMsec_(this.connectTime_ - this.startTime_));
165 | }
166 | if (rtt !== null) {
167 | contents += this.buildLine_('RTT', InfoBox.formatMsec_(rtt));
168 | }
169 | if (e2eDelay !== null) {
170 | contents += this.buildLine_('End to end', InfoBox.formatMsec_(e2eDelay));
171 | }
172 |
173 | // Obtain resolution, framerate, and bitrate this.stats_.
174 | // TODO(juberti): find a better way to tell these apart.
175 | var txAudio = getStatsReport(this.stats_, 'ssrc', 'audioInputLevel');
176 | var rxAudio = getStatsReport(this.stats_, 'ssrc', 'audioOutputLevel');
177 | var txVideo = getStatsReport(this.stats_, 'ssrc', 'googFirsReceived');
178 | var rxVideo = getStatsReport(this.stats_, 'ssrc', 'googFirsSent');
179 | var txPrevAudio = getStatsReport(this.prevStats_, 'ssrc', 'audioInputLevel');
180 | var rxPrevAudio = getStatsReport(this.prevStats_, 'ssrc', 'audioOutputLevel');
181 | var txPrevVideo = getStatsReport(this.prevStats_, 'ssrc', 'googFirsReceived');
182 | var rxPrevVideo = getStatsReport(this.prevStats_, 'ssrc', 'googFirsSent');
183 | var txAudioCodec;
184 | var txAudioBitrate;
185 | var txAudioPacketRate;
186 | var rxAudioCodec;
187 | var rxAudioBitrate;
188 | var rxAudioPacketRate;
189 | var txVideoHeight;
190 | var txVideoFps;
191 | var txVideoCodec;
192 | var txVideoBitrate;
193 | var txVideoPacketRate;
194 | var rxVideoHeight;
195 | var rxVideoFps;
196 | var rxVideoCodec;
197 | var rxVideoBitrate;
198 | var rxVideoPacketRate;
199 | if (txAudio) {
200 | txAudioCodec = txAudio.stat('googCodecName');
201 | txAudioBitrate = computeBitrate(txAudio, txPrevAudio, 'bytesSent');
202 | txAudioPacketRate = computeRate(txAudio, txPrevAudio, 'packetsSent');
203 | contents += this.buildLine_('Audio Tx', txAudioCodec + ', ' +
204 | InfoBox.formatBitrate_(txAudioBitrate) + ', ' +
205 | InfoBox.formatPacketRate_(txAudioPacketRate));
206 | }
207 | if (rxAudio) {
208 | rxAudioCodec = rxAudio.stat('googCodecName');
209 | rxAudioBitrate = computeBitrate(rxAudio, rxPrevAudio, 'bytesReceived');
210 | rxAudioPacketRate = computeRate(rxAudio, rxPrevAudio, 'packetsReceived');
211 | contents += this.buildLine_('Audio Rx', rxAudioCodec + ', ' +
212 | InfoBox.formatBitrate_(rxAudioBitrate) + ', ' +
213 | InfoBox.formatPacketRate_(rxAudioPacketRate));
214 | }
215 | if (txVideo) {
216 | txVideoCodec = txVideo.stat('googCodecName');
217 | txVideoHeight = txVideo.stat('googFrameHeightSent');
218 | txVideoFps = txVideo.stat('googFrameRateSent');
219 | txVideoBitrate = computeBitrate(txVideo, txPrevVideo, 'bytesSent');
220 | txVideoPacketRate = computeRate(txVideo, txPrevVideo, 'packetsSent');
221 | contents += this.buildLine_('Video Tx',
222 | txVideoCodec + ', ' + txVideoHeight.toString() + 'p' +
223 | txVideoFps.toString() + ', ' +
224 | InfoBox.formatBitrate_(txVideoBitrate) + ', ' +
225 | InfoBox.formatPacketRate_(txVideoPacketRate));
226 | }
227 | if (rxVideo) {
228 | rxVideoCodec = 'TODO'; // rxVideo.stat('googCodecName');
229 | rxVideoHeight = this.remoteVideo_.videoHeight;
230 | // TODO(juberti): this should ideally be obtained from the video element.
231 | rxVideoFps = rxVideo.stat('googFrameRateDecoded');
232 | rxVideoBitrate = computeBitrate(rxVideo, rxPrevVideo, 'bytesReceived');
233 | rxVideoPacketRate = computeRate(rxVideo, rxPrevVideo, 'packetsReceived');
234 | contents += this.buildLine_('Video Rx',
235 | rxVideoCodec + ', ' + rxVideoHeight.toString() + 'p' +
236 | rxVideoFps.toString() + ', ' +
237 | InfoBox.formatBitrate_(rxVideoBitrate) + ', ' +
238 | InfoBox.formatPacketRate_(rxVideoPacketRate));
239 | }
240 | return contents;
241 | };
242 |
243 | InfoBox.prototype.buildLine_ = function(label, value) {
244 | var columnWidth = 12;
245 | var line = '';
246 | if (label) {
247 | line += label + ':';
248 | while (line.length < columnWidth) {
249 | line += ' ';
250 | }
251 |
252 | if (value) {
253 | line += value;
254 | }
255 | }
256 | line += '\n';
257 | return line;
258 | };
259 |
260 | // Convert a number of milliseconds into a '[HH:]MM:SS' string.
261 | InfoBox.formatInterval_ = function(value) {
262 | var result = '';
263 | var seconds = Math.floor(value / 1000);
264 | var minutes = Math.floor(seconds / 60);
265 | var hours = Math.floor(minutes / 60);
266 | var formatTwoDigit = function(twodigit) {
267 | return ((twodigit < 10) ? '0' : '') + twodigit.toString();
268 | };
269 |
270 | if (hours > 0) {
271 | result += formatTwoDigit(hours) + ':';
272 | }
273 | result += formatTwoDigit(minutes - hours * 60) + ':';
274 | result += formatTwoDigit(seconds - minutes * 60);
275 | return result;
276 | };
277 |
278 | // Convert a number of milliesconds into a 'XXX ms' string.
279 | InfoBox.formatMsec_ = function(value) {
280 | return value.toFixed(0).toString() + ' ms';
281 | };
282 |
283 | // Convert a bitrate into a 'XXX Xbps' string.
284 | InfoBox.formatBitrate_ = function(value) {
285 | if (!value) {
286 | return '- bps';
287 | }
288 |
289 | var suffix;
290 | if (value < 1000) {
291 | suffix = 'bps';
292 | } else if (value < 1000000) {
293 | suffix = 'kbps';
294 | value /= 1000;
295 | } else {
296 | suffix = 'Mbps';
297 | value /= 1000000;
298 | }
299 |
300 | var str = value.toPrecision(3) + ' ' + suffix;
301 | return str;
302 | };
303 |
304 | // Convert a packet rate into a 'XXX pps' string.
305 | InfoBox.formatPacketRate_ = function(value) {
306 | if (!value) {
307 | return '- pps';
308 | }
309 | return value.toPrecision(3) + ' ' + 'pps';
310 | };
311 |
--------------------------------------------------------------------------------
/public/js/peerconnectionclient_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals TestCase, assertEquals, assertNotNull, assertTrue, assertFalse,
12 | PeerConnectionClient */
13 |
14 | 'use strict';
15 |
16 | var FAKEPCCONFIG = {
17 | 'bar': 'foo'
18 | };
19 | var FAKEPCCONSTRAINTS = {
20 | 'foo': 'bar'
21 | };
22 |
23 | var peerConnections = [];
24 | var MockRTCPeerConnection = function(config, constraints) {
25 | this.config = config;
26 | this.constraints = constraints;
27 | this.streams = [];
28 | this.createSdpRequests = [];
29 | this.localDescriptions = [];
30 | this.remoteDescriptions = [];
31 | this.remoteIceCandidates = [];
32 | this.signalingState = 'stable';
33 |
34 | peerConnections.push(this);
35 | };
36 | MockRTCPeerConnection.prototype.addStream = function(stream) {
37 | this.streams.push(stream);
38 | };
39 | MockRTCPeerConnection.prototype.createOffer =
40 | function(callback, errback, constraints) {
41 | this.createSdpRequests.push({
42 | type: 'offer',
43 | callback: callback,
44 | errback: errback,
45 | constraints: constraints
46 | });
47 | };
48 | MockRTCPeerConnection.prototype.createAnswer =
49 | function(callback, errback, constraints) {
50 | this.createSdpRequests.push({
51 | type: 'answer',
52 | callback: callback,
53 | errback: errback,
54 | constraints: constraints
55 | });
56 | };
57 | MockRTCPeerConnection.prototype.resolveLastCreateSdpRequest = function(sdp) {
58 | var request = this.createSdpRequests.pop();
59 | assertNotNull(request);
60 |
61 | if (sdp) {
62 | request.callback({
63 | 'type': request.type,
64 | 'sdp': sdp
65 | });
66 | } else {
67 | request.errback(Error('MockCreateSdpError'));
68 | }
69 | };
70 | MockRTCPeerConnection.prototype.setLocalDescription =
71 | function(localDescription, callback, errback) {
72 | if (localDescription.type === 'offer') {
73 | this.signalingState = 'have-local-offer';
74 | } else {
75 | this.signalingState = 'stable';
76 | }
77 | this.localDescriptions.push({
78 | description: localDescription,
79 | callback: callback,
80 | errback: errback
81 | });
82 | };
83 | MockRTCPeerConnection.prototype.setRemoteDescription =
84 | function(remoteDescription, callback, errback) {
85 | if (remoteDescription.type === 'offer') {
86 | this.signalingState = 'have-remote-offer';
87 | } else {
88 | this.signalingState = 'stable';
89 | }
90 | this.remoteDescriptions.push({
91 | description: remoteDescription,
92 | callback: callback,
93 | errback: errback
94 | });
95 | };
96 | MockRTCPeerConnection.prototype.addIceCandidate = function(candidate) {
97 | this.remoteIceCandidates.push(candidate);
98 | };
99 | MockRTCPeerConnection.prototype.close = function() {
100 | this.signalingState = 'closed';
101 | };
102 | MockRTCPeerConnection.prototype.getRemoteStreams = function() {
103 | return [{
104 | getVideoTracks: function() { return ['track']; }
105 | }];
106 | };
107 |
108 | function getParams(pcConfig, pcConstraints) {
109 | return {
110 | 'peerConnectionConfig': pcConfig,
111 | 'peerConnectionConstraints': pcConstraints
112 | };
113 | }
114 |
115 | var PeerConnectionClientTest = new TestCase('PeerConnectionClientTest');
116 |
117 | PeerConnectionClientTest.prototype.setUp = function() {
118 | window.params = {};
119 |
120 | this.readlRTCPeerConnection = RTCPeerConnection;
121 | RTCPeerConnection = MockRTCPeerConnection;
122 |
123 | peerConnections.length = 0;
124 | this.pcClient = new PeerConnectionClient(
125 | getParams(FAKEPCCONFIG, FAKEPCCONSTRAINTS), window.performance.now());
126 | };
127 |
128 | PeerConnectionClientTest.prototype.tearDown = function() {
129 | RTCPeerConnection = this.readlRTCPeerConnection;
130 | };
131 |
132 | PeerConnectionClientTest.prototype.testConstructor = function() {
133 | assertEquals(1, peerConnections.length);
134 | assertEquals(FAKEPCCONFIG, peerConnections[0].config);
135 | assertEquals(FAKEPCCONSTRAINTS, peerConnections[0].constraints);
136 | };
137 |
138 | PeerConnectionClientTest.prototype.testAddStream = function() {
139 | var stream = {'foo': 'bar'};
140 | this.pcClient.addStream(stream);
141 | assertEquals(1, peerConnections[0].streams.length);
142 | assertEquals(stream, peerConnections[0].streams[0]);
143 | };
144 |
145 | PeerConnectionClientTest.prototype.testStartAsCaller = function() {
146 | var signalingMsgs = [];
147 | function onSignalingMessage(msg) {
148 | signalingMsgs.push(msg);
149 | }
150 |
151 | this.pcClient.onsignalingmessage = onSignalingMessage;
152 | assertTrue(this.pcClient.startAsCaller(null));
153 |
154 | assertEquals(1, peerConnections[0].createSdpRequests.length);
155 | var request = peerConnections[0].createSdpRequests[0];
156 | assertEquals('offer', request.type);
157 |
158 | var fakeSdp = 'fake sdp';
159 | peerConnections[0].resolveLastCreateSdpRequest(fakeSdp);
160 |
161 | // Verify the input to setLocalDesciption.
162 | assertEquals(1, peerConnections[0].localDescriptions.length);
163 | assertEquals('offer',
164 | peerConnections[0].localDescriptions[0].description.type);
165 | assertEquals(fakeSdp,
166 | peerConnections[0].localDescriptions[0].description.sdp);
167 |
168 | // Verify the output signaling message for the offer.
169 | assertEquals(1, signalingMsgs.length);
170 | assertEquals('offer', signalingMsgs[0].type);
171 | assertEquals(fakeSdp, signalingMsgs[0].sdp);
172 |
173 | // Verify the output signaling messages for the ICE candidates.
174 | signalingMsgs.length = 0;
175 | var fakeCandidate = 'fake candidate';
176 | var event = {
177 | candidate: {
178 | sdpMLineIndex: '0',
179 | sdpMid: '1',
180 | candidate: fakeCandidate
181 | }
182 | };
183 | var expectedMessage = {
184 | type: 'candidate',
185 | label: event.candidate.sdpMLineIndex,
186 | id: event.candidate.sdpMid,
187 | candidate: event.candidate.candidate
188 | };
189 | peerConnections[0].onicecandidate(event);
190 | assertEquals(1, signalingMsgs.length);
191 | assertEquals(expectedMessage, signalingMsgs[0]);
192 | };
193 |
194 | PeerConnectionClientTest.prototype.testCallerReceiveSignalingMessage =
195 | function() {
196 | this.pcClient.startAsCaller(null);
197 | peerConnections[0].resolveLastCreateSdpRequest('fake offer');
198 | var remoteAnswer = {
199 | type: 'answer',
200 | sdp: 'fake answer'
201 | };
202 |
203 | var pc = peerConnections[0];
204 |
205 | this.pcClient.receiveSignalingMessage(JSON.stringify(remoteAnswer));
206 | assertEquals(1, pc.remoteDescriptions.length);
207 | assertEquals('answer', pc.remoteDescriptions[0].description.type);
208 | assertEquals(remoteAnswer.sdp, pc.remoteDescriptions[0].description.sdp);
209 |
210 | var candidate = {
211 | type: 'candidate',
212 | label: '0',
213 | candidate: 'fake candidate'
214 | };
215 | this.pcClient.receiveSignalingMessage(JSON.stringify(candidate));
216 | assertEquals(1, pc.remoteIceCandidates.length);
217 | assertEquals(candidate.label, pc.remoteIceCandidates[0].sdpMLineIndex);
218 | assertEquals(candidate.candidate, pc.remoteIceCandidates[0].candidate);
219 | };
220 |
221 | PeerConnectionClientTest.prototype.testStartAsCallee = function() {
222 | var remoteOffer = {
223 | type: 'offer',
224 | sdp: 'fake sdp'
225 | };
226 | var candidate = {
227 | type: 'candidate',
228 | label: '0',
229 | candidate: 'fake candidate'
230 | };
231 | var initialMsgs = [
232 | JSON.stringify(candidate),
233 | JSON.stringify(remoteOffer)
234 | ];
235 | this.pcClient.startAsCallee(initialMsgs);
236 |
237 | var pc = peerConnections[0];
238 |
239 | // Verify that remote offer and ICE candidates are set.
240 | assertEquals(1, pc.remoteDescriptions.length);
241 | assertEquals('offer', pc.remoteDescriptions[0].description.type);
242 | assertEquals(remoteOffer.sdp, pc.remoteDescriptions[0].description.sdp);
243 | assertEquals(1, pc.remoteIceCandidates.length);
244 | assertEquals(candidate.label, pc.remoteIceCandidates[0].sdpMLineIndex);
245 | assertEquals(candidate.candidate, pc.remoteIceCandidates[0].candidate);
246 |
247 | // Verify that createAnswer is called.
248 | assertEquals(1, pc.createSdpRequests.length);
249 | assertEquals('answer', pc.createSdpRequests[0].type);
250 |
251 | var fakeAnswer = 'fake answer';
252 | pc.resolveLastCreateSdpRequest(fakeAnswer);
253 |
254 | // Verify that setLocalDescription is called.
255 | assertEquals(1, pc.localDescriptions.length);
256 | assertEquals('answer', pc.localDescriptions[0].description.type);
257 | assertEquals(fakeAnswer, pc.localDescriptions[0].description.sdp);
258 | };
259 |
260 | PeerConnectionClient.prototype.testReceiveRemoteOfferBeforeStarted =
261 | function() {
262 | var remoteOffer = {
263 | type: 'offer',
264 | sdp: 'fake sdp'
265 | };
266 | this.pcClient.receiveSignalingMessage(JSON.stringify(remoteOffer));
267 | this.pcClient.startAsCallee(null);
268 |
269 | // Verify that the offer received before started is processed.
270 | var pc = peerConnections[0];
271 | assertEquals(1, pc.remoteDescriptions.length);
272 | assertEquals('offer', pc.remoteDescriptions[0].description.type);
273 | assertEquals(remoteOffer.sdp, pc.remoteDescriptions[0].description.sdp);
274 | };
275 |
276 | PeerConnectionClientTest.prototype.testRemoteHangup = function() {
277 | var remoteHangup = false;
278 | this.pcClient.onremotehangup = function() {
279 | remoteHangup = true;
280 | };
281 | this.pcClient.receiveSignalingMessage(JSON.stringify({
282 | type: 'bye'
283 | }));
284 | assertTrue(remoteHangup);
285 | };
286 |
287 | PeerConnectionClientTest.prototype.testOnRemoteSdpSet = function() {
288 | var hasRemoteTrack = false;
289 | function onRemoteSdpSet(result) {
290 | hasRemoteTrack = result;
291 | }
292 | this.pcClient.onremotesdpset = onRemoteSdpSet;
293 |
294 | var remoteOffer = {
295 | type: 'offer',
296 | sdp: 'fake sdp'
297 | };
298 | var initialMsgs = [JSON.stringify(remoteOffer)];
299 | this.pcClient.startAsCallee(initialMsgs);
300 |
301 | var callback = peerConnections[0].remoteDescriptions[0].callback;
302 | assertNotNull(callback);
303 | callback();
304 | assertTrue(hasRemoteTrack);
305 | };
306 |
307 | PeerConnectionClientTest.prototype.testOnRemoteStreamAdded = function() {
308 | var stream = null;
309 | function onRemoteStreamAdded(s) {
310 | stream = s;
311 | }
312 | this.pcClient.onremotestreamadded = onRemoteStreamAdded;
313 |
314 | var event = {
315 | stream: 'stream'
316 | };
317 | peerConnections[0].onaddstream(event);
318 | assertEquals(event.stream, stream);
319 | };
320 |
321 | PeerConnectionClientTest.prototype.testOnSignalingStateChange = function() {
322 | var called = false;
323 | function callback() {
324 | called = true;
325 | }
326 | this.pcClient.onsignalingstatechange = callback;
327 | peerConnections[0].onsignalingstatechange();
328 | assertTrue(called);
329 | };
330 |
331 | PeerConnectionClientTest.prototype.testOnIceConnectionStateChange = function() {
332 | var called = false;
333 | function callback() {
334 | called = true;
335 | }
336 | this.pcClient.oniceconnectionstatechange = callback;
337 | peerConnections[0].oniceconnectionstatechange();
338 | assertTrue(called);
339 | };
340 |
341 | PeerConnectionClientTest.prototype.testStartAsCallerTwiceFailed = function() {
342 | assertTrue(this.pcClient.startAsCaller(null));
343 | assertFalse(this.pcClient.startAsCaller(null));
344 | };
345 |
346 | PeerConnectionClientTest.prototype.testStartAsCalleeTwiceFailed = function() {
347 | assertTrue(this.pcClient.startAsCallee(null));
348 | assertFalse(this.pcClient.startAsCallee(null));
349 | };
350 |
351 | PeerConnectionClientTest.prototype.testClose = function() {
352 | this.pcClient.close();
353 | assertEquals('closed', peerConnections[0].signalingState);
354 | };
355 |
--------------------------------------------------------------------------------
/public/js/roomselection_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals UI_CONSTANTS, RoomSelection, assertMatch, assertEquals,
12 | AsyncTestCase */
13 |
14 | 'use strict';
15 |
16 | var RoomSelectionTest = new AsyncTestCase('RoomSelectionTest');
17 |
18 | RoomSelectionTest.prototype.setUp = function() {
19 | var key = 'testRecentRoomsKey';
20 | localStorage.removeItem(key);
21 | localStorage.setItem(key, '["room1", "room2", "room3"]');
22 |
23 | this.targetDiv_ = document.createElement('div');
24 | this.targetDiv_.id = UI_CONSTANTS.roomSelectionDiv.substring(1);
25 |
26 | this.inputBox_ = document.createElement('input');
27 | this.inputBox_.id = UI_CONSTANTS.roomSelectionInput.substring(1);
28 | this.inputBox_.type = 'text';
29 |
30 | this.inputBoxLabel_ = document.createElement('label');
31 | this.inputBoxLabel_.id = UI_CONSTANTS.roomSelectionInputLabel.substring(1);
32 |
33 | this.randomButton_ = document.createElement('button');
34 | this.randomButton_.id = UI_CONSTANTS.roomSelectionRandomButton.substring(1);
35 |
36 | this.joinButton_ = document.createElement('button');
37 | this.joinButton_.id = UI_CONSTANTS.roomSelectionJoinButton.substring(1);
38 |
39 | this.recentList_ = document.createElement('ul');
40 | this.recentList_.id = UI_CONSTANTS.roomSelectionRecentList.substring(1);
41 |
42 | this.targetDiv_.appendChild(this.inputBox_);
43 | this.targetDiv_.appendChild(this.inputBoxLabel_);
44 | this.targetDiv_.appendChild(this.randomButton_);
45 | this.targetDiv_.appendChild(this.joinButton_);
46 | this.targetDiv_.appendChild(this.recentList_);
47 |
48 | this.roomSelectionSetupCompletedPromise_ = new Promise(function(resolve) {
49 | this.roomSelection_ = new RoomSelection(this.targetDiv_,
50 | UI_CONSTANTS,
51 | key,
52 | function() {
53 | resolve();
54 | }.bind(this));
55 | }.bind(this));
56 | };
57 |
58 | RoomSelectionTest.prototype.tearDown = function() {
59 | localStorage.removeItem('testRecentRoomsKey');
60 | this.roomSelection_ = null;
61 | };
62 |
63 | RoomSelectionTest.createUIEvent = function(type) {
64 | var event = document.createEvent('UIEvent');
65 | event.initUIEvent(type, true, true);
66 | return event;
67 | };
68 |
69 | RoomSelectionTest.prototype.testInputFilter = function() {
70 | var validInputs = [
71 | '123123',
72 | 'asdfs3',
73 | 'room1',
74 | '3254234523452345234523452345asdfasfdasdf'
75 | ];
76 | var invalidInputs = [
77 | '',
78 | ' ',
79 | 'abcd',
80 | '123',
81 | '[5afasdf',
82 | 'ñsaer3'
83 | ];
84 |
85 | var testInput = function(input, expectedResult) {
86 | this.inputBox_.value = input;
87 | this.inputBox_.dispatchEvent(RoomSelectionTest.createUIEvent('input'));
88 |
89 | assertEquals('Incorrect result with input: "' + input + '"',
90 | expectedResult,
91 | this.joinButton_.disabled);
92 | }.bind(this);
93 |
94 | for (var i = 0; i < validInputs.length; ++i) {
95 | testInput(validInputs[i], false);
96 | }
97 |
98 | for (i = 0; i < invalidInputs.length; ++i) {
99 | testInput(invalidInputs[i], true);
100 | }
101 | };
102 |
103 | RoomSelectionTest.prototype.testRandomButton = function() {
104 | this.inputBox_.value = '123';
105 | this.randomButton_.click();
106 | assertMatch(/[0-9]{9}/, this.inputBox_.value);
107 | };
108 |
109 | RoomSelectionTest.prototype.testRecentListHasChildren = function(queue) {
110 | queue.call('Step 1: wait for recent rooms list to be completed.',
111 | function(callbacks) {
112 | var onCompleted = callbacks.add(function() {});
113 | this.roomSelectionSetupCompletedPromise_.then(function() {
114 | onCompleted();
115 | }.bind(this));
116 | });
117 |
118 | queue.call('Step 2: validate recent rooms list.', function() {
119 | var children = this.recentList_.children;
120 | assertEquals('There should be 3 recent links.', 3, children.length);
121 | assertEquals('The text of the first should be room4.',
122 | 'room1',
123 | children[0].innerText);
124 | assertEquals('The first link should have 1 child.',
125 | 1,
126 | children[0].children.length);
127 | assertMatch('That child should be an href with a link containing room1.', /room1/, children[0].children[0].href);
128 | });
129 | };
130 |
131 | RoomSelectionTest.prototype.testJoinButton = function() {
132 | this.inputBox_.value = 'targetRoom';
133 | var joinedRoom = null;
134 | this.roomSelection_.onRoomSelected = function(room) {
135 | joinedRoom = room;
136 | };
137 | this.joinButton_.click();
138 |
139 | assertEquals('targetRoom', joinedRoom);
140 | };
141 |
142 | RoomSelectionTest.prototype.testMakeClickHandler = function(queue) {
143 | queue.call('Step 1: wait for recent rooms list to be completed.',
144 | function(callbacks) {
145 | var onCompleted = callbacks.add(function() {});
146 | this.roomSelectionSetupCompletedPromise_.then(function() {
147 | onCompleted();
148 | }.bind(this));
149 | });
150 |
151 | queue.call('Step 2: validate that click handler works.', function() {
152 | var children = this.recentList_.children;
153 | var link = children[0].children[0];
154 |
155 | var joinedRoom = null;
156 | this.roomSelection_.onRoomSelected = function(room) {
157 | joinedRoom = room;
158 | };
159 |
160 | link.dispatchEvent(RoomSelectionTest.createUIEvent('click'));
161 |
162 | assertEquals('room1', joinedRoom);
163 | });
164 | };
165 |
166 | RoomSelectionTest.prototype.testMatchRandomRoomPattern = function() {
167 | var testCases = [
168 | 'abcdefghi',
169 | '1abcdefgh',
170 | '1abcdefg1',
171 | '12345678',
172 | '12345678a',
173 | 'a12345678',
174 | '123456789'
175 | ];
176 | var expected = [
177 | false, false, false, false, false, false, true
178 | ];
179 | for (var i = 0; i < testCases.length; ++i) {
180 | assertEquals(expected[i],
181 | RoomSelection.matchRandomRoomPattern(testCases[i]));
182 | }
183 | };
184 |
185 | RoomSelectionTest.prototype.testHitEnterInRoomIdInput = function() {
186 | var joinedRoom = null;
187 | this.roomSelection_.onRoomSelected = function(room) {
188 | joinedRoom = room;
189 | };
190 | function createEnterKeyUpEvent() {
191 | var e = document.createEvent('Event');
192 | e.initEvent('keyup');
193 | e.keyCode = 13;
194 | e.which = 13;
195 | return e;
196 | }
197 |
198 | // Hitting ENTER when the room name is invalid should do nothing.
199 | this.inputBox_.value = '1';
200 | this.inputBox_.dispatchEvent(RoomSelectionTest.createUIEvent('input'));
201 | this.inputBox_.dispatchEvent(createEnterKeyUpEvent());
202 | assertEquals(null, joinedRoom);
203 |
204 | // Hitting ENTER when the room name is valid should select the room.
205 | this.inputBox_.value = '12345';
206 | this.inputBox_.dispatchEvent(RoomSelectionTest.createUIEvent('input'));
207 | assertEquals(false, this.joinButton_.disabled);
208 | this.inputBox_.dispatchEvent(createEnterKeyUpEvent());
209 | assertEquals(this.inputBox_.value, joinedRoom);
210 |
211 | joinedRoom = null;
212 | // Hitting other keys should not select the room.
213 | var e = document.createEvent('Event');
214 | e.initEvent('keyup');
215 | this.inputBox_.dispatchEvent(e);
216 | assertEquals(null, joinedRoom);
217 | };
218 |
219 | var RecentlyUsedListTest = new AsyncTestCase('RecentlyUsedListTest');
220 |
221 | RecentlyUsedListTest.prototype.setUp = function() {
222 | this.key_ = 'testRecentRoomsKey';
223 |
224 | this.fullList_ =
225 | '["room4","room5","room6","room7","room8","room9",' +
226 | '"room10","room11","room12","room13"]';
227 | this.tooManyList_ =
228 | '["room1","room2","room3","room4","room5","room6",' +
229 | '"room7","room8","room9","room10","room11","room12","room13"]';
230 | this.duplicatesList_ =
231 | '["room4","room4","room6","room7","room6","room9",' +
232 | '"room10","room4","room6","room13"]';
233 | this.noDuplicatesList_ =
234 | '["room4","room6","room7","room9","room10","room13"]';
235 | this.emptyList_ = '[]';
236 | this.notAList_ = 'asdasd';
237 |
238 | this.recentlyUsedList_ = new RoomSelection.RecentlyUsedList(this.key_);
239 | };
240 |
241 | RecentlyUsedListTest.prototype.tearDown = function() {
242 | localStorage.removeItem(this.key_);
243 | this.recentlyUsedList_ = null;
244 | };
245 |
246 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomDuplicateList =
247 | function(queue) {
248 | queue.call('Step 1: push new value.', function(callbacks) {
249 | var onCompleted = callbacks.add(function() {});
250 | localStorage.removeItem(this.key_);
251 | localStorage.setItem(this.key_, this.duplicatesList_);
252 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() {
253 | onCompleted();
254 | }.bind(this));
255 | });
256 | queue.call('Step 2: verify results.', function() {
257 | var result = localStorage.getItem(this.key_);
258 | assertEquals(
259 | this.noDuplicatesList_
260 | .replace('"room4"', '"newRoom","room4"'),
261 | result);
262 | });
263 | };
264 |
265 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomTooManyList =
266 | function(queue) {
267 | queue.call('Step 1: push new value.', function(callbacks) {
268 | var onCompleted = callbacks.add(function() {});
269 | localStorage.removeItem(this.key_);
270 | localStorage.setItem(this.key_, this.tooManyList_);
271 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() {
272 | onCompleted();
273 | }.bind(this));
274 | });
275 | queue.call('Step 2: verify results.', function() {
276 | var result = localStorage.getItem(this.key_);
277 | assertEquals(
278 | this.tooManyList_
279 | .replace(',"room10","room11","room12","room13"', '')
280 | .replace('"room1"', '"newRoom","room1"'),
281 | result);
282 | });
283 | };
284 |
285 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomFullList =
286 | function(queue) {
287 | queue.call('Step 1: push new value.', function(callbacks) {
288 | var onCompleted = callbacks.add(function() {});
289 | localStorage.removeItem(this.key_);
290 | localStorage.setItem(this.key_, this.fullList_);
291 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() {
292 | onCompleted();
293 | }.bind(this));
294 | });
295 | queue.call('Step 2: verify results.', function() {
296 | var result = localStorage.getItem(this.key_);
297 | assertEquals(
298 | this.fullList_
299 | .replace(',"room13"', '')
300 | .replace('"room4"', '"newRoom","room4"'),
301 | result);
302 | });
303 | };
304 |
305 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomNoExisting =
306 | function(queue) {
307 | queue.call('Step 1: push new value.', function(callbacks) {
308 | var onCompleted = callbacks.add(function() {});
309 | localStorage.removeItem(this.key_);
310 |
311 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() {
312 | onCompleted();
313 | }.bind(this));
314 | });
315 | queue.call('Step 2: verify results.', function() {
316 | var result = localStorage.getItem(this.key_);
317 | assertEquals('["newRoom"]', result);
318 | });
319 | };
320 |
321 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomInvalidExisting =
322 | function(queue) {
323 | queue.call('Step 1: push new value.', function(callbacks) {
324 | var onCompleted = callbacks.add(function() {});
325 | localStorage.removeItem(this.key_);
326 | localStorage.setItem(this.key_, this.notAList_);
327 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() {
328 | onCompleted();
329 | }.bind(this));
330 | });
331 | queue.call('Step 2: verify results.', function() {
332 | var result = localStorage.getItem(this.key_);
333 | assertEquals('["newRoom"]', result);
334 | });
335 | };
336 |
--------------------------------------------------------------------------------
/public/js/sdputils.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals trace */
12 | /* exported setCodecParam, iceCandidateType, maybeSetOpusOptions,
13 | maybePreferAudioReceiveCodec, maybePreferAudioSendCodec,
14 | maybeSetAudioReceiveBitRate, maybeSetAudioSendBitRate,
15 | maybePreferVideoReceiveCodec, maybePreferVideoSendCodec,
16 | maybeSetVideoReceiveBitRate, maybeSetVideoSendBitRate,
17 | maybeSetVideoSendInitialBitRate, mergeConstraints, removeCodecParam */
18 |
19 | 'use strict';
20 |
21 | function mergeConstraints(cons1, cons2) {
22 | if (!cons1 || !cons2) {
23 | return cons1 || cons2;
24 | }
25 | var merged = cons1;
26 | for (var name in cons2.mandatory) {
27 | merged.mandatory[name] = cons2.mandatory[name];
28 | }
29 | merged.optional = merged.optional.concat(cons2.optional);
30 | return merged;
31 | }
32 |
33 | function iceCandidateType(candidateStr) {
34 | return candidateStr.split(' ')[7];
35 | }
36 |
37 | function maybeSetOpusOptions(sdp, params) {
38 | // Set Opus in Stereo, if stereo is true, unset it, if stereo is false, and
39 | // do nothing if otherwise.
40 | if (params.opusStereo === 'true') {
41 | sdp = setCodecParam(sdp, 'opus/48000', 'stereo', '1');
42 | } else if (params.opusStereo === 'false') {
43 | sdp = removeCodecParam(sdp, 'opus/48000', 'stereo');
44 | }
45 |
46 | // Set Opus FEC, if opusfec is true, unset it, if opusfec is false, and
47 | // do nothing if otherwise.
48 | if (params.opusFec === 'true') {
49 | sdp = setCodecParam(sdp, 'opus/48000', 'useinbandfec', '1');
50 | } else if (params.opusFec === 'false') {
51 | sdp = removeCodecParam(sdp, 'opus/48000', 'useinbandfec');
52 | }
53 |
54 | // Set Opus maxplaybackrate, if requested.
55 | if (params.opusMaxPbr) {
56 | sdp = setCodecParam(
57 | sdp, 'opus/48000', 'maxplaybackrate', params.opusMaxPbr);
58 | }
59 | return sdp;
60 | }
61 |
62 | function maybeSetAudioSendBitRate(sdp, params) {
63 | if (!params.audioSendBitrate) {
64 | return sdp;
65 | }
66 | trace('Prefer audio send bitrate: ' + params.audioSendBitrate);
67 | return preferBitRate(sdp, params.audioSendBitrate, 'audio');
68 | }
69 |
70 | function maybeSetAudioReceiveBitRate(sdp, params) {
71 | if (!params.audioRecvBitrate) {
72 | return sdp;
73 | }
74 | trace('Prefer audio receive bitrate: ' + params.audioRecvBitrate);
75 | return preferBitRate(sdp, params.audioRecvBitrate, 'audio');
76 | }
77 |
78 | function maybeSetVideoSendBitRate(sdp, params) {
79 | if (!params.videoSendBitrate) {
80 | return sdp;
81 | }
82 | trace('Prefer video send bitrate: ' + params.videoSendBitrate);
83 | return preferBitRate(sdp, params.videoSendBitrate, 'video');
84 | }
85 |
86 | function maybeSetVideoReceiveBitRate(sdp, params) {
87 | if (!params.videoRecvBitrate) {
88 | return sdp;
89 | }
90 | trace('Prefer video receive bitrate: ' + params.videoRecvBitrate);
91 | return preferBitRate(sdp, params.videoRecvBitrate, 'video');
92 | }
93 |
94 | // Add a b=AS:bitrate line to the m=mediaType section.
95 | function preferBitRate(sdp, bitrate, mediaType) {
96 | var sdpLines = sdp.split('\r\n');
97 |
98 | // Find m line for the given mediaType.
99 | var mLineIndex = findLine(sdpLines, 'm=', mediaType);
100 | if (mLineIndex === null) {
101 | trace('Failed to add bandwidth line to sdp, as no m-line found');
102 | return sdp;
103 | }
104 |
105 | // Find next m-line if any.
106 | var nextMLineIndex = findLineInRange(sdpLines, mLineIndex + 1, -1, 'm=');
107 | if (nextMLineIndex === null) {
108 | nextMLineIndex = sdpLines.length;
109 | }
110 |
111 | // Find c-line corresponding to the m-line.
112 | var cLineIndex = findLineInRange(sdpLines, mLineIndex + 1,
113 | nextMLineIndex, 'c=');
114 | if (cLineIndex === null) {
115 | trace('Failed to add bandwidth line to sdp, as no c-line found');
116 | return sdp;
117 | }
118 |
119 | // Check if bandwidth line already exists between c-line and next m-line.
120 | var bLineIndex = findLineInRange(sdpLines, cLineIndex + 1,
121 | nextMLineIndex, 'b=AS');
122 | if (bLineIndex) {
123 | sdpLines.splice(bLineIndex, 1);
124 | }
125 |
126 | // Create the b (bandwidth) sdp line.
127 | var bwLine = 'b=AS:' + bitrate;
128 | // As per RFC 4566, the b line should follow after c-line.
129 | sdpLines.splice(cLineIndex + 1, 0, bwLine);
130 | sdp = sdpLines.join('\r\n');
131 | return sdp;
132 | }
133 |
134 | // Add an a=fmtp: x-google-min-bitrate=kbps line, if videoSendInitialBitrate
135 | // is specified. We'll also add a x-google-min-bitrate value, since the max
136 | // must be >= the min.
137 | function maybeSetVideoSendInitialBitRate(sdp, params) {
138 | var initialBitrate = params.videoSendInitialBitrate;
139 | if (!initialBitrate) {
140 | return sdp;
141 | }
142 |
143 | // Validate the initial bitrate value.
144 | var maxBitrate = initialBitrate;
145 | var bitrate = params.videoSendBitrate;
146 | if (bitrate) {
147 | if (initialBitrate > bitrate) {
148 | trace('Clamping initial bitrate to max bitrate of ' +
149 | bitrate + ' kbps.');
150 | initialBitrate = bitrate;
151 | params.videoSendInitialBitrate = initialBitrate;
152 | }
153 | maxBitrate = bitrate;
154 | }
155 |
156 | var sdpLines = sdp.split('\r\n');
157 |
158 | // Search for m line.
159 | var mLineIndex = findLine(sdpLines, 'm=', 'video');
160 | if (mLineIndex === null) {
161 | trace('Failed to find video m-line');
162 | return sdp;
163 | }
164 |
165 | sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-min-bitrate',
166 | params.videoSendInitialBitrate.toString());
167 | sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-max-bitrate',
168 | maxBitrate.toString());
169 |
170 | return sdp;
171 | }
172 |
173 | // Promotes |audioSendCodec| to be the first in the m=audio line, if set.
174 | function maybePreferAudioSendCodec(sdp, params) {
175 | return maybePreferCodec(sdp, 'audio', 'send', params.audioSendCodec);
176 | }
177 |
178 | // Promotes |audioRecvCodec| to be the first in the m=audio line, if set.
179 | function maybePreferAudioReceiveCodec(sdp, params) {
180 | return maybePreferCodec(sdp, 'audio', 'receive', params.audioRecvCodec);
181 | }
182 |
183 | // Promotes |videoSendCodec| to be the first in the m=audio line, if set.
184 | function maybePreferVideoSendCodec(sdp, params) {
185 | return maybePreferCodec(sdp, 'video', 'send', params.videoSendCodec);
186 | }
187 |
188 | // Promotes |videoRecvCodec| to be the first in the m=audio line, if set.
189 | function maybePreferVideoReceiveCodec(sdp, params) {
190 | return maybePreferCodec(sdp, 'video', 'receive', params.videoRecvCodec);
191 | }
192 |
193 | // Sets |codec| as the default |type| codec if it's present.
194 | // The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'.
195 | function maybePreferCodec(sdp, type, dir, codec) {
196 | var str = type + ' ' + dir + ' codec';
197 | if (codec === '') {
198 | trace('No preference on ' + str + '.');
199 | return sdp;
200 | }
201 |
202 | trace('Prefer ' + str + ': ' + codec);
203 |
204 | var sdpLines = sdp.split('\r\n');
205 |
206 | // Search for m line.
207 | var mLineIndex = findLine(sdpLines, 'm=', type);
208 | if (mLineIndex === null) {
209 | return sdp;
210 | }
211 |
212 | // If the codec is available, set it as the default in m line.
213 | var payload = getCodecPayloadType(sdpLines, codec);
214 | if (payload) {
215 | sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
216 | }
217 |
218 | sdp = sdpLines.join('\r\n');
219 | return sdp;
220 | }
221 |
222 | // Set fmtp param to specific codec in SDP. If param does not exists, add it.
223 | function setCodecParam(sdp, codec, param, value) {
224 | var sdpLines = sdp.split('\r\n');
225 |
226 | var fmtpLineIndex = findFmtpLine(sdpLines, codec);
227 |
228 | var fmtpObj = {};
229 | if (fmtpLineIndex === null) {
230 | var index = findLine(sdpLines, 'a=rtpmap', codec);
231 | if (index === null) {
232 | return sdp;
233 | }
234 | var payload = getCodecPayloadTypeFromLine(sdpLines[index]);
235 | fmtpObj.pt = payload.toString();
236 | fmtpObj.params = {};
237 | fmtpObj.params[param] = value;
238 | sdpLines.splice(index + 1, 0, writeFmtpLine(fmtpObj));
239 | } else {
240 | fmtpObj = parseFmtpLine(sdpLines[fmtpLineIndex]);
241 | fmtpObj.params[param] = value;
242 | sdpLines[fmtpLineIndex] = writeFmtpLine(fmtpObj);
243 | }
244 |
245 | sdp = sdpLines.join('\r\n');
246 | return sdp;
247 | }
248 |
249 | // Remove fmtp param if it exists.
250 | function removeCodecParam(sdp, codec, param) {
251 | var sdpLines = sdp.split('\r\n');
252 |
253 | var fmtpLineIndex = findFmtpLine(sdpLines, codec);
254 | if (fmtpLineIndex === null) {
255 | return sdp;
256 | }
257 |
258 | var map = parseFmtpLine(sdpLines[fmtpLineIndex]);
259 | delete map.params[param];
260 |
261 | var newLine = writeFmtpLine(map);
262 | if (newLine === null) {
263 | sdpLines.splice(fmtpLineIndex, 1);
264 | } else {
265 | sdpLines[fmtpLineIndex] = newLine;
266 | }
267 |
268 | sdp = sdpLines.join('\r\n');
269 | return sdp;
270 | }
271 |
272 | // Split an fmtp line into an object including 'pt' and 'params'.
273 | function parseFmtpLine(fmtpLine) {
274 | var fmtpObj = {};
275 | var spacePos = fmtpLine.indexOf(' ');
276 | var keyValues = fmtpLine.substring(spacePos + 1).split('; ');
277 |
278 | var pattern = new RegExp('a=fmtp:(\\d+)');
279 | var result = fmtpLine.match(pattern);
280 | if (result && result.length === 2) {
281 | fmtpObj.pt = result[1];
282 | } else {
283 | return null;
284 | }
285 |
286 | var params = {};
287 | for (var i = 0; i < keyValues.length; ++i) {
288 | var pair = keyValues[i].split('=');
289 | if (pair.length === 2) {
290 | params[pair[0]] = pair[1];
291 | }
292 | }
293 | fmtpObj.params = params;
294 |
295 | return fmtpObj;
296 | }
297 |
298 | // Generate an fmtp line from an object including 'pt' and 'params'.
299 | function writeFmtpLine(fmtpObj) {
300 | if (!fmtpObj.hasOwnProperty('pt') || !fmtpObj.hasOwnProperty('params')) {
301 | return null;
302 | }
303 | var pt = fmtpObj.pt;
304 | var params = fmtpObj.params;
305 | var keyValues = [];
306 | var i = 0;
307 | for (var key in params) {
308 | keyValues[i] = key + '=' + params[key];
309 | ++i;
310 | }
311 | if (i === 0) {
312 | return null;
313 | }
314 | return 'a=fmtp:' + pt.toString() + ' ' + keyValues.join('; ');
315 | }
316 |
317 | // Find fmtp attribute for |codec| in |sdpLines|.
318 | function findFmtpLine(sdpLines, codec) {
319 | // Find payload of codec.
320 | var payload = getCodecPayloadType(sdpLines, codec);
321 | // Find the payload in fmtp line.
322 | return payload ? findLine(sdpLines, 'a=fmtp:' + payload.toString()) : null;
323 | }
324 |
325 | // Find the line in sdpLines that starts with |prefix|, and, if specified,
326 | // contains |substr| (case-insensitive search).
327 | function findLine(sdpLines, prefix, substr) {
328 | return findLineInRange(sdpLines, 0, -1, prefix, substr);
329 | }
330 |
331 | // Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
332 | // and, if specified, contains |substr| (case-insensitive search).
333 | function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
334 | var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
335 | for (var i = startLine; i < realEndLine; ++i) {
336 | if (sdpLines[i].indexOf(prefix) === 0) {
337 | if (!substr ||
338 | sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
339 | return i;
340 | }
341 | }
342 | }
343 | return null;
344 | }
345 |
346 | // Gets the codec payload type from sdp lines.
347 | function getCodecPayloadType(sdpLines, codec) {
348 | var index = findLine(sdpLines, 'a=rtpmap', codec);
349 | return index ? getCodecPayloadTypeFromLine(sdpLines[index]) : null;
350 | }
351 |
352 | // Gets the codec payload type from an a=rtpmap:X line.
353 | function getCodecPayloadTypeFromLine(sdpLine) {
354 | var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
355 | var result = sdpLine.match(pattern);
356 | return (result && result.length === 2) ? result[1] : null;
357 | }
358 |
359 | // Returns a new m= line with the specified codec as the first one.
360 | function setDefaultCodec(mLine, payload) {
361 | var elements = mLine.split(' ');
362 |
363 | // Just copy the first three parameters; codec order starts on fourth.
364 | var newLine = elements.slice(0, 3);
365 |
366 | // Put target payload first and copy in the rest.
367 | newLine.push(payload);
368 | for (var i = 3; i < elements.length; i++) {
369 | if (elements[i] !== payload) {
370 | newLine.push(elements[i]);
371 | }
372 | }
373 | return newLine.join(' ');
374 | }
375 |
--------------------------------------------------------------------------------
/public/js/peerconnectionclient.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree.
7 | */
8 |
9 | /* More information about these options at jshint.com/docs/options */
10 |
11 | /* globals trace, mergeConstraints, parseJSON, iceCandidateType,
12 | maybePreferAudioReceiveCodec, maybePreferVideoReceiveCodec,
13 | maybePreferAudioSendCodec, maybePreferVideoSendCodec,
14 | maybeSetAudioSendBitRate, maybeSetVideoSendBitRate,
15 | maybeSetAudioReceiveBitRate, maybeSetVideoSendInitialBitRate,
16 | maybeSetVideoReceiveBitRate, maybeSetVideoSendInitialBitRate,
17 | maybeSetOpusOptions */
18 |
19 | /* exported PeerConnectionClient */
20 |
21 | 'use strict';
22 |
23 | var PeerConnectionClient = function(params, startTime) {
24 | this.params_ = params;
25 | this.startTime_ = startTime;
26 |
27 | trace('Creating RTCPeerConnnection with:\n' +
28 | ' config: \'' + JSON.stringify(params.peerConnectionConfig) + '\';\n' +
29 | ' constraints: \'' + JSON.stringify(params.peerConnectionConstraints) +
30 | '\'.');
31 |
32 | // Create an RTCPeerConnection via the polyfill (adapter.js).
33 | this.pc_ = new RTCPeerConnection(
34 | params.peerConnectionConfig, params.peerConnectionConstraints);
35 | this.pc_.onicecandidate = this.onIceCandidate_.bind(this);
36 | this.pc_.onaddstream = this.onRemoteStreamAdded_.bind(this);
37 | this.pc_.onremovestream = trace.bind(null, 'Remote stream removed.');
38 | this.pc_.onsignalingstatechange = this.onSignalingStateChanged_.bind(this);
39 | this.pc_.oniceconnectionstatechange =
40 | this.onIceConnectionStateChanged_.bind(this);
41 |
42 | this.hasRemoteSdp_ = false;
43 | this.messageQueue_ = [];
44 | this.isInitiator_ = false;
45 | this.started_ = false;
46 |
47 | // TODO(jiayl): Replace callbacks with events.
48 | // Public callbacks. Keep it sorted.
49 | this.onerror = null;
50 | this.oniceconnectionstatechange = null;
51 | this.onnewicecandidate = null;
52 | this.onremotehangup = null;
53 | this.onremotesdpset = null;
54 | this.onremotestreamadded = null;
55 | this.onsignalingmessage = null;
56 | this.onsignalingstatechange = null;
57 | };
58 |
59 | // Set up audio and video regardless of what devices are present.
60 | // Disable comfort noise for maximum audio quality.
61 | PeerConnectionClient.DEFAULT_SDP_CONSTRAINTS_ = {
62 | 'mandatory': {
63 | 'OfferToReceiveAudio': true,
64 | 'OfferToReceiveVideo': true
65 | },
66 | 'optional': [{
67 | 'VoiceActivityDetection': false
68 | }]
69 | };
70 |
71 | PeerConnectionClient.prototype.addStream = function(stream) {
72 | if (!this.pc_) {
73 | return;
74 | }
75 | this.pc_.addStream(stream);
76 | };
77 |
78 | PeerConnectionClient.prototype.startAsCaller = function(offerConstraints) {
79 | if (!this.pc_) {
80 | return false;
81 | }
82 |
83 | if (this.started_) {
84 | return false;
85 | }
86 |
87 | this.isInitiator_ = true;
88 | this.started_ = true;
89 | var constraints = mergeConstraints(
90 | offerConstraints, PeerConnectionClient.DEFAULT_SDP_CONSTRAINTS_);
91 | trace('Sending offer to peer, with constraints: \n\'' +
92 | JSON.stringify(constraints) + '\'.');
93 | this.pc_.createOffer(this.setLocalSdpAndNotify_.bind(this),
94 | this.onError_.bind(this, 'createOffer'),
95 | constraints);
96 |
97 | return true;
98 | };
99 |
100 | PeerConnectionClient.prototype.startAsCallee = function(initialMessages) {
101 | if (!this.pc_) {
102 | return false;
103 | }
104 |
105 | if (this.started_) {
106 | return false;
107 | }
108 |
109 | this.isInitiator_ = false;
110 | this.started_ = true;
111 |
112 | if (initialMessages && initialMessages.length > 0) {
113 | // Convert received messages to JSON objects and add them to the message
114 | // queue.
115 | for (var i = 0, len = initialMessages.length; i < len; i++) {
116 | this.receiveSignalingMessage(initialMessages[i]);
117 | }
118 | return true;
119 | }
120 |
121 | // We may have queued messages received from the signaling channel before
122 | // started.
123 | if (this.messageQueue_.length > 0) {
124 | this.drainMessageQueue_();
125 | }
126 | return true;
127 | };
128 |
129 | PeerConnectionClient.prototype.receiveSignalingMessage = function(message) {
130 | var messageObj = parseJSON(message);
131 | if (!messageObj) {
132 | return;
133 | }
134 | if ((this.isInitiator_ && messageObj.type === 'answer') ||
135 | (!this.isInitiator_ && messageObj.type === 'offer')) {
136 | this.hasRemoteSdp_ = true;
137 | // Always process offer before candidates.
138 | this.messageQueue_.unshift(messageObj);
139 | } else if (messageObj.type === 'candidate') {
140 | this.messageQueue_.push(messageObj);
141 | } else if (messageObj.type === 'bye') {
142 | if (this.onremotehangup) {
143 | this.onremotehangup();
144 | }
145 | }
146 | this.drainMessageQueue_();
147 | };
148 |
149 | PeerConnectionClient.prototype.close = function() {
150 | if (!this.pc_) {
151 | return;
152 | }
153 | this.pc_.close();
154 | this.pc_ = null;
155 | };
156 |
157 | PeerConnectionClient.prototype.getPeerConnectionStates = function() {
158 | if (!this.pc_) {
159 | return null;
160 | }
161 | return {
162 | 'signalingState': this.pc_.signalingState,
163 | 'iceGatheringState': this.pc_.iceGatheringState,
164 | 'iceConnectionState': this.pc_.iceConnectionState
165 | };
166 | };
167 |
168 | PeerConnectionClient.prototype.getPeerConnectionStats = function(callback) {
169 | if (!this.pc_) {
170 | return;
171 | }
172 | this.pc_.getStats(callback);
173 | };
174 |
175 | PeerConnectionClient.prototype.doAnswer_ = function() {
176 | trace('Sending answer to peer.');
177 | this.pc_.createAnswer(this.setLocalSdpAndNotify_.bind(this),
178 | this.onError_.bind(this, 'createAnswer'),
179 | PeerConnectionClient.DEFAULT_SDP_CONSTRAINTS_);
180 | };
181 |
182 | PeerConnectionClient.prototype.setLocalSdpAndNotify_ =
183 | function(sessionDescription) {
184 | sessionDescription.sdp = maybePreferAudioReceiveCodec(
185 | sessionDescription.sdp,
186 | this.params_);
187 | sessionDescription.sdp = maybePreferVideoReceiveCodec(
188 | sessionDescription.sdp,
189 | this.params_);
190 | sessionDescription.sdp = maybeSetAudioReceiveBitRate(
191 | sessionDescription.sdp,
192 | this.params_);
193 | sessionDescription.sdp = maybeSetVideoReceiveBitRate(
194 | sessionDescription.sdp,
195 | this.params_);
196 | this.pc_.setLocalDescription(sessionDescription,
197 | trace.bind(null, 'Set session description success.'),
198 | this.onError_.bind(this, 'setLocalDescription'));
199 |
200 | if (this.onsignalingmessage) {
201 | this.onsignalingmessage(sessionDescription);
202 | }
203 | };
204 |
205 | PeerConnectionClient.prototype.setRemoteSdp_ = function(message) {
206 | message.sdp = maybeSetOpusOptions(message.sdp, this.params_);
207 | message.sdp = maybePreferAudioSendCodec(message.sdp, this.params_);
208 | message.sdp = maybePreferVideoSendCodec(message.sdp, this.params_);
209 | message.sdp = maybeSetAudioSendBitRate(message.sdp, this.params_);
210 | message.sdp = maybeSetVideoSendBitRate(message.sdp, this.params_);
211 | message.sdp = maybeSetVideoSendInitialBitRate(message.sdp, this.params_);
212 | this.pc_.setRemoteDescription(new RTCSessionDescription(message),
213 | this.onSetRemoteDescriptionSuccess_.bind(this),
214 | this.onError_.bind(this, 'setRemoteDescription'));
215 | };
216 |
217 | PeerConnectionClient.prototype.onSetRemoteDescriptionSuccess_ = function() {
218 | trace('Set remote session description success.');
219 | // By now all onaddstream events for the setRemoteDescription have fired,
220 | // so we can know if the peer has any remote video streams that we need
221 | // to wait for. Otherwise, transition immediately to the active state.
222 | var remoteStreams = this.pc_.getRemoteStreams();
223 | if (this.onremotesdpset) {
224 | this.onremotesdpset(remoteStreams.length > 0 &&
225 | remoteStreams[0].getVideoTracks().length > 0);
226 | }
227 | };
228 |
229 | PeerConnectionClient.prototype.processSignalingMessage_ = function(message) {
230 | if (message.type === 'offer' && !this.isInitiator_) {
231 | if (this.pc_.signalingState !== 'stable') {
232 | trace('ERROR: remote offer received in unexpected state: ' +
233 | this.pc_.signalingState);
234 | return;
235 | }
236 | this.setRemoteSdp_(message);
237 | this.doAnswer_();
238 | } else if (message.type === 'answer' && this.isInitiator_) {
239 | if (this.pc_.signalingState !== 'have-local-offer') {
240 | trace('ERROR: remote answer received in unexpected state: ' +
241 | this.pc_.signalingState);
242 | return;
243 | }
244 | this.setRemoteSdp_(message);
245 | } else if (message.type === 'candidate') {
246 | var candidate = new RTCIceCandidate({
247 | sdpMLineIndex: message.label,
248 | candidate: message.candidate
249 | });
250 | this.recordIceCandidate_('Remote', candidate);
251 | this.pc_.addIceCandidate(candidate,
252 | trace.bind(null, 'Remote candidate added successfully.'),
253 | this.onError_.bind(this, 'addIceCandidate'));
254 | } else {
255 | trace('WARNING: unexpected message: ' + JSON.stringify(message));
256 | }
257 | };
258 |
259 | // When we receive messages from GAE registration and from the WSS connection,
260 | // we add them to a queue and drain it if conditions are right.
261 | PeerConnectionClient.prototype.drainMessageQueue_ = function() {
262 | // It's possible that we finish registering and receiving messages from WSS
263 | // before our peer connection is created or started. We need to wait for the
264 | // peer connection to be created and started before processing messages.
265 | //
266 | // Also, the order of messages is in general not the same as the POST order
267 | // from the other client because the POSTs are async and the server may handle
268 | // some requests faster than others. We need to process offer before
269 | // candidates so we wait for the offer to arrive first if we're answering.
270 | // Offers are added to the front of the queue.
271 | if (!this.pc_ || !this.started_ || !this.hasRemoteSdp_) {
272 | return;
273 | }
274 | for (var i = 0, len = this.messageQueue_.length; i < len; i++) {
275 | this.processSignalingMessage_(this.messageQueue_[i]);
276 | }
277 | this.messageQueue_ = [];
278 | };
279 |
280 | PeerConnectionClient.prototype.onIceCandidate_ = function(event) {
281 | if (event.candidate) {
282 | // Eat undesired candidates.
283 | if (this.filterIceCandidate_(event.candidate)) {
284 | var message = {
285 | type: 'candidate',
286 | label: event.candidate.sdpMLineIndex,
287 | id: event.candidate.sdpMid,
288 | candidate: event.candidate.candidate
289 | };
290 | if (this.onsignalingmessage) {
291 | this.onsignalingmessage(message);
292 | }
293 | this.recordIceCandidate_('Local', event.candidate);
294 | }
295 | } else {
296 | trace('End of candidates.');
297 | }
298 | };
299 |
300 | PeerConnectionClient.prototype.onSignalingStateChanged_ = function() {
301 | if (!this.pc_) {
302 | return;
303 | }
304 | trace('Signaling state changed to: ' + this.pc_.signalingState);
305 |
306 | if (this.onsignalingstatechange) {
307 | this.onsignalingstatechange();
308 | }
309 | };
310 |
311 | PeerConnectionClient.prototype.onIceConnectionStateChanged_ = function() {
312 | if (!this.pc_) {
313 | return;
314 | }
315 | trace('ICE connection state changed to: ' + this.pc_.iceConnectionState);
316 | if (this.pc_.iceConnectionState === 'completed') {
317 | trace('ICE complete time: ' +
318 | (window.performance.now() - this.startTime_).toFixed(0) + 'ms.');
319 | }
320 |
321 | if (this.oniceconnectionstatechange) {
322 | this.oniceconnectionstatechange();
323 | }
324 | };
325 |
326 | // Return false if the candidate should be dropped, true if not.
327 | PeerConnectionClient.prototype.filterIceCandidate_ = function(candidateObj) {
328 | var candidateStr = candidateObj.candidate;
329 |
330 | // Always eat TCP candidates. Not needed in this context.
331 | if (candidateStr.indexOf('tcp') !== -1) {
332 | return false;
333 | }
334 |
335 | // If we're trying to eat non-relay candidates, do that.
336 | if (this.params_.peerConnectionConfig.iceTransports === 'relay' &&
337 | iceCandidateType(candidateStr) !== 'relay') {
338 | return false;
339 | }
340 |
341 | return true;
342 | };
343 |
344 | PeerConnectionClient.prototype.recordIceCandidate_ =
345 | function(location, candidateObj) {
346 | if (this.onnewicecandidate) {
347 | this.onnewicecandidate(location, candidateObj.candidate);
348 | }
349 | };
350 |
351 | PeerConnectionClient.prototype.onRemoteStreamAdded_ = function(event) {
352 | if (this.onremotestreamadded) {
353 | this.onremotestreamadded(event.stream);
354 | }
355 | };
356 |
357 | PeerConnectionClient.prototype.onError_ = function(tag, error) {
358 | if (this.onerror) {
359 | this.onerror(tag + ': ' + error.toString());
360 | }
361 | };
362 |
--------------------------------------------------------------------------------