├── .gitignore
├── LICENSE
├── README.md
├── webapp
    ├── index.html
    ├── package.json
    ├── src
    │   ├── UIElements
    │   │   └── LoginForm.tsx
    │   ├── Utils.ts
    │   ├── app.scss
    │   ├── app.tsx
    │   └── img
    │   │   └── icon.png
    ├── tsconfig.json
    └── webpack.config.js
└── webservice
    └── auth.php
/.gitignore:
--------------------------------------------------------------------------------
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | 
 6 | # Runtime data
 7 | pids
 8 | *.pid
 9 | *.seed
10 | 
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 | 
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 | 
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 | 
20 | # node-waf configuration
21 | .lock-wscript
22 | 
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 | 
26 | # Dependency directory
27 | node_modules
28 | 
29 | # Optional npm cache directory
30 | .npm
31 | 
32 | # Optional REPL history
33 | .node_repl_history
34 | 
35 | #MAC
36 | .DS_Store
37 | typings
38 | 
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright (c) 2016 Alexey Aylarov
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # VoxImplant Client Side Conferencing using P2P Calls and Web Audio
 2 | Webapp folder contains web application built using Web SDK, ReactJS and TypeScript, it can act like conference host and can be connected to the conference by inbound call.
 3 | Webservice folder contains PHP script used to proxy requests to VoxImplant HTTP API to create users and authorization.
 4 | 
 5 | ## Important Tips
 6 | 1. Don't forget to replace YOUR_API_KEY, YOUR_ACCOUNT_NAME, APP_NAME and SOME_PASSWORD with your data in `auth.php`
 7 | 2. Don't forget to replace YOUR_VOX_APPNAME, YOUR_VOX_ACCNAME and YOUR_DOMAIN with your data in `app.tsx`
 8 | 
 9 | ### Building and running
10 | 
11 | The webapp uses webpack:
12 | 
13 | 1. npm install
14 | 2. webpack
15 | 
16 | 
--------------------------------------------------------------------------------
/webapp/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 | 	VoxImplant Client Conferencing 
 6 | 	
11 | 	
12 | 	
13 | 		
14 | 
15 | 
--------------------------------------------------------------------------------
/webapp/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "voximplant-clientconf-app",
 3 |   "version": "0.0.1",
 4 |   "description": "VoxImplant Client Conferencing",
 5 |   "keywords": [
 6 |     "react",
 7 |     "reactjs",
 8 |     "webpack",
 9 |     "voximplant"
10 |   ],
11 |   "author": "Alexey Aylarov  (http://github.com/aylarov)",
12 |   "license": "MIT",
13 |   "bugs": {
14 |     "url": "https://github.com/aylarov/voxclientconf/issues"
15 |   },
16 |   "postinstall": "typings install voximplant-websdk && typings install jquery --ambient && typings install react --ambient && typings install react-dom --ambient && typings install react-bootstrap --ambient",
17 |   "homepage": "https://github.com/aylarov/voxclientconf",
18 |   "dependencies": {
19 |     "css-loader": "^0.23.1",
20 |     "es6-promise": "^3.1.2",
21 |     "file-loader": "^0.8.5",
22 |     "jquery": "^2.1.4",
23 |     "node-sass": "^3.4.1",
24 |     "postcss": "^5.0.16",
25 |     "postcss-loader": "^0.8.1",
26 |     "react": "^0.14.7",
27 |     "react-bootstrap": "^0.28.3",
28 |     "react-dom": "^0.14.7",
29 |     "react-hot-loader": "^1.3.0",
30 |     "sass-loader": "^3.1.2",
31 |     "ts-jsx-loader": "^0.2.1",
32 |     "ts-loader": "^0.8.1",
33 |     "typescript": "^1.8.0",
34 |     "url-loader": "^0.5.7",
35 |     "voximplant-websdk": "^3.6.294",
36 |     "webpack": "^1.12.2",
37 |     "webpack-dev-server": "^1.12.1"
38 |   }
39 | }
40 | 
--------------------------------------------------------------------------------
/webapp/src/UIElements/LoginForm.tsx:
--------------------------------------------------------------------------------
 1 | declare function require(string): any;
 2 | 
 3 | import * as React from 'react';
 4 | import * as ReactDOM from 'react-dom';
 5 | import $ = require('jquery');
 6 | import {
 7 | 	Alert,
 8 | 	Button,
 9 | 	Panel,
10 | 	Input
11 | } from 'react-bootstrap';
12 | 
13 | interface Props {
14 | 	onSubmit: (displayName: string) => void;
15 | 	ref: string;
16 | }
17 | 
18 | class LoginForm extends React.Component {
19 | 
20 |   constructor(props: Props) {
21 |     super(props);
22 |   }
23 | 
24 |   private componentDidMount() {
25 |   	var el = this.refs["loginForm"];
26 |   	$(el).submit(function(event) {
27 |       let displayName = this.refs["displayName"].getValue();
28 |       this.props.onSubmit(displayName);
29 |   		event.preventDefault();
30 | 	  }.bind(this));
31 |     $("#display_name_input").focus();
32 |   }
33 | 
34 |   render() {
35 |     return (
36 |     	
37 |     		
41 |   		 	 
42 |     );
43 |   }
44 | }
45 | 
46 | export default LoginForm;
--------------------------------------------------------------------------------
/webapp/src/Utils.ts:
--------------------------------------------------------------------------------
 1 | import $ = require('jquery');
 2 | 
 3 | export default class Utils {
 4 | 	static generateId() {
 5 | 		let maximum = 999999,
 6 | 			minimum = 0,
 7 | 			conferenceId = Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
 8 | 		return conferenceId;
 9 | 	}
10 | 
11 | 	static getNameFromURI(uri: string) {
12 | 		if (uri.indexOf('@') != -1) uri = uri.substr(0, uri.indexOf('@'));
13 | 		uri = uri.replace("sip:", "");
14 | 		return uri;
15 | 	}
16 | 
17 | 	static queryString() {
18 | 		let query_string = {},
19 | 			query = window.location.search.substring(1),
20 | 			vars = query.split("&");
21 | 		for (let i = 0; i < vars.length; i++) {
22 | 			let pair = vars[i].split("=");
23 | 			if (typeof query_string[pair[0]] === "undefined") {
24 | 				query_string[pair[0]] = pair[1];
25 | 			} else if (typeof query_string[pair[0]] === "string") {
26 | 				let arr = [query_string[pair[0]], pair[1]];
27 | 				query_string[pair[0]] = arr;
28 | 			} else {
29 | 				query_string[pair[0]].push(pair[1]);
30 | 			}
31 | 		}
32 | 		return query_string;
33 | 	}	
34 | }
--------------------------------------------------------------------------------
/webapp/src/app.scss:
--------------------------------------------------------------------------------
  1 | html, body {
  2 | 	display: flex;
  3 | 	align-items: center;
  4 |   justify-content: center;  	
  5 |   height: 100%;
  6 |   width: 100%;
  7 | }
  8 | 
  9 | #app.conference {
 10 |   width: 100%;
 11 |   height: 100%;
 12 |   display: flex;
 13 |   flex-direction: column;
 14 | }
 15 | 
 16 | .tip {
 17 |   font-size: 16pt;
 18 | }
 19 | 
 20 | div.ex-container {
 21 |   display: flex;
 22 |   height: 100%;
 23 | }
 24 | 
 25 | #container {
 26 |   padding: 0;
 27 |   margin: 0;
 28 |   flex: 1;
 29 |   display: flex;
 30 |   justify-content: center;
 31 |   align-items: center;
 32 |   flex-direction: column;  
 33 | }
 34 | 
 35 | div.loginForm {
 36 | 	width: 400px;
 37 | 	text-align: center;
 38 | }
 39 | 
 40 | h4.modal-title {
 41 |   padding-left: 32px;
 42 |   background: url(./img/icon.png) no-repeat left center;
 43 |   background-size: 28px 28px;
 44 | }
 45 | 
 46 | .spinner2 {
 47 |   margin-top: 15px;
 48 |   text-align: center;
 49 | }
 50 | 
 51 | .spinner2 > div {
 52 |   width: 18px;
 53 |   height: 18px;
 54 |   background-color: #000;
 55 | 
 56 |   border-radius: 100%;
 57 |   display: inline-block;
 58 |   -webkit-animation: bouncedelay 1.4s infinite ease-in-out;
 59 |   animation: bouncedelay 1.4s infinite ease-in-out;
 60 |   /* Prevent first frame from flickering when animation starts */
 61 |   -webkit-animation-fill-mode: both;
 62 |   animation-fill-mode: both;
 63 | }
 64 | 
 65 | .spinner2 .bounce1 {
 66 |   -webkit-animation-delay: -0.32s;
 67 |   animation-delay: -0.32s;
 68 | }
 69 | 
 70 | .spinner2 .bounce2 {
 71 |   -webkit-animation-delay: -0.16s;
 72 |   animation-delay: -0.16s;
 73 | }
 74 | 
 75 | @-webkit-keyframes bouncedelay {
 76 |   0%, 80%, 100% { -webkit-transform: scale(0.0) }
 77 |   40% { -webkit-transform: scale(1.0) }
 78 | }
 79 | 
 80 | @keyframes bouncedelay {
 81 |   0%, 80%, 100% { 
 82 |     transform: scale(0.0);
 83 |     -webkit-transform: scale(0.0);
 84 |   } 40% { 
 85 |     transform: scale(1.0);
 86 |     -webkit-transform: scale(1.0);
 87 |   }
 88 | }
 89 | 
 90 | @-webkit-keyframes glow {
 91 |     to {
 92 |          border-color: #000;
 93 |     -webkit-box-shadow: 0 0 5px #000;
 94 |        -moz-box-shadow: 0 0 5px #000;
 95 |             box-shadow: 0 0 5px #000;
 96 |     }
 97 | }
 98 | 
 99 | @keyframes glow {
100 |   to {
101 |          border-color: #000;
102 |     -webkit-box-shadow: 0 0 5px #000;
103 |        -moz-box-shadow: 0 0 5px #000;
104 |             box-shadow: 0 0 5px #000;
105 |     }
106 | }
107 | 
108 | .spinner {
109 |   display: inline-block;
110 |   opacity: 0;
111 |   width: 0;
112 |   margin-right: 5px;
113 | 
114 |   -webkit-transition: opacity 0.25s, width 0.25s;
115 |   -moz-transition: opacity 0.25s, width 0.25s;
116 |   -o-transition: opacity 0.25s, width 0.25s;
117 |   transition: opacity 0.25s, width 0.25s;
118 | }
119 | 
120 | .has-spinner.active {
121 |   cursor:progress;
122 | }
123 | 
124 | .has-spinner.active .spinner {
125 |   opacity: 1;
126 |   width: auto; /* This doesn't work, just fix for unkown width elements */
127 |   animation: spin 2s infinite linear;
128 |   -webkit-animation: spin2 2s infinite linear;
129 | }
130 | 
131 | .has-spinner.btn-mini.active .spinner {
132 |     width: 10px;
133 | }
134 | 
135 | .has-spinner.btn-small.active .spinner {
136 |     width: 13px;
137 | }
138 | 
139 | .has-spinner.btn.active .spinner {
140 |     width: 16px;
141 | }
142 | 
143 | .has-spinner.btn-large.active .spinner {
144 |     width: 19px;
145 | }
146 | 
147 | @keyframes spin {
148 |     from { transform: scale(1) rotate(0deg); }
149 |     to { transform: scale(1) rotate(360deg); }
150 | }
151 | 
152 | @-webkit-keyframes spin2 {
153 |     from { -webkit-transform: rotate(0deg); }
154 |     to { -webkit-transform: rotate(360deg); }
155 | }
--------------------------------------------------------------------------------
/webapp/src/app.tsx:
--------------------------------------------------------------------------------
  1 | declare function require(string): string;
  2 | import $ = require('jquery');
  3 | import jQuery = require('jquery');
  4 | import * as React from 'react';
  5 | import * as ReactDOM from 'react-dom';
  6 | import * as VoxImplant from 'voximplant-websdk';
  7 | import LoginForm from "./UIElements/LoginForm";
  8 | import {
  9 | 	Button,
 10 | 	Modal,
 11 | 	Input,
 12 | 	Row,
 13 | 	Col,
 14 | 	ListGroup,
 15 | 	ListGroupItem
 16 | } from 'react-bootstrap';
 17 | import Utils from './Utils';
 18 | require('./app.scss');
 19 | 
 20 | enum AppViews {
 21 | 	INIT,
 22 | 	CONNECTED,
 23 | 	AUTH,
 24 | 	CONFERENCE_PARTICIPANTS,
 25 | 	CONFERENCE_CALLING,
 26 | 	INBOUND,
 27 | 	FINISHED
 28 | }
 29 | 
 30 | enum CallStatuses {
 31 | 	DEFAULT,
 32 | 	INIT,
 33 | 	CONNECTED,
 34 | 	STREAM_CONNECTED,
 35 | 	ENDED
 36 | }
 37 | 
 38 | interface State {
 39 | 	view: AppViews,
 40 | 	tip?: String
 41 | }
 42 | 
 43 | class Mix {
 44 | 	/**
 45 | 	* AudioContext
 46 | 	*/
 47 | 	audioCtx: AudioContext;		
 48 | 	/**
 49 | 	* Audio channels merger
 50 | 	*/
 51 | 	merger: ChannelMergerNode;
 52 | 	splitter: ChannelSplitterNode;
 53 | 	/**
 54 | 	* User for whom this mix is created for
 55 | 	*/
 56 | 	forUser: string;
 57 | 	/**
 58 | 	* Destination
 59 | 	*/
 60 | 	destination: any;
 61 | 	/**
 62 | 	* Participants whos steams were already added to the mix
 63 | 	*/
 64 | 	pariticipants: string[];
 65 | 	host: boolean;
 66 | 
 67 | 	constructor(for_user: string, localstream: MediaStream, audiocontext: AudioContext, host: boolean = false) {
 68 | 		this.audioCtx = audiocontext;
 69 | 		this.pariticipants = [];
 70 | 		this.forUser = for_user;		
 71 | 		this.merger = this.audioCtx.createChannelMerger();
 72 | 		this.host = host;
 73 | 		if (!host) {
 74 | 			this.destination = this.audioCtx.createMediaStreamDestination();
 75 | 		} else {		
 76 | 			console.log("Send local mix to audio device");	
 77 | 			this.destination = this.audioCtx.destination;
 78 | 			//this.merger.connect(this.destination);
 79 | 		}
 80 | 		if (!host) {
 81 | 			this.audioCtx.createMediaStreamSource(localstream).connect(this.merger, 0, 0);
 82 | 			this.audioCtx.createMediaStreamSource(localstream).connect(this.merger, 0, 1);
 83 | 		}
 84 | 		console.log("MIX[" + this.forUser + "] created");
 85 | 	}
 86 | 
 87 | 	addParticipant(name: string, mediastream: MediaStream) {
 88 | 		let found: boolean = false;
 89 | 		for (let i = 0; i < this.pariticipants.length; i++) {
 90 | 			if (this.pariticipants[i] == name) {
 91 | 				found = true;
 92 | 				break;
 93 | 			}
 94 | 		}
 95 | 		if (!found) {
 96 | 			let source: MediaStreamAudioSourceNode = this.audioCtx.createMediaStreamSource(mediastream);
 97 | 			source.connect(this.merger, 0, 0);
 98 | 			source.connect(this.merger, 0, 1);
 99 | 			this.pariticipants.push(name);
100 | 			console.log("MIX[" + this.forUser + "] mediastreams:");
101 | 			console.log(this.pariticipants);
102 | 		}
103 | 	}
104 | 
105 | 	getResultStream() {		
106 | 		this.merger.connect(this.destination);
107 | 		return this.destination.stream;
108 | 	}
109 | 
110 | }
111 | 
112 | class App extends React.Component {
113 | 
114 | 	// SDK instance
115 | 	voxAPI: VoxImplant.Client;
116 | 
117 | 	// Account info
118 | 	displayName: string;
119 | 	username: string;
120 | 	appname: string = YOUR_VOX_APPNAME;
121 | 	accname: string = YOUR_VOX_ACCNAME;
122 | 	
123 | 	// Roster data
124 | 	roster: VoxImplant.RosterItem[];
125 | 	presence: Object[];
126 | 
127 | 	peerCalls: VoxImplant.Call[];
128 | 	mixes: Mix[];
129 | 	QueryString: Object;
130 | 	participants: Object[];
131 | 
132 | 	localStream: MediaStream;
133 | 	peerStreams: MediaStream[] = [];
134 | 	host: boolean = false;
135 | 	audioCtx: AudioContext;
136 | 	calls: number = 0;
137 | 	wsURL: string = 'https://'+YOUR_DOMAIN+'/auth.php';
138 | 
139 | 	state: State = {
140 | 		view: AppViews.INIT,
141 | 		tip: "Please allow access to your camera and microphone"
142 | 	}
143 | 
144 | 	constructor() {
145 | 		super();
146 | 		this.QueryString = Utils.queryString();
147 | 		this.peerCalls = [];
148 | 		this.participants = [];
149 | 		this.mixes = [];
150 | 		this.audioCtx = new AudioContext();
151 | 		this.roster = [];
152 | 		this.presence = [];
153 | 		this.voxAPI = VoxImplant.getInstance();
154 | 		// Init
155 | 		this.voxAPI.addEventListener(VoxImplant.Events.SDKReady, (e: VoxImplant.Events.SDKReady) => this.voxReady(e));
156 | 		// Connection
157 | 		this.voxAPI.addEventListener(VoxImplant.Events.ConnectionEstablished, (e: VoxImplant.Events.ConnectionEstablished) => this.voxConnected(e));
158 | 		this.voxAPI.addEventListener(VoxImplant.Events.ConnectionFailed, (e: VoxImplant.Events.ConnectionFailed) => this.voxConnectionFailed(e));
159 | 		this.voxAPI.addEventListener(VoxImplant.Events.ConnectionClosed, (e: VoxImplant.Events.ConnectionClosed) => this.voxConnectionClosed(e));
160 | 		// Auth
161 | 		this.voxAPI.addEventListener(VoxImplant.Events.AuthResult, (e: VoxImplant.Events.AuthResult) => this.voxAuthEvent(e));
162 | 		// Misc 
163 | 		this.voxAPI.addEventListener(VoxImplant.Events.MicAccessResult, (e: VoxImplant.Events.MicAccessResult) => this.voxMicAccessResult(e));
164 | 		this.voxAPI.addEventListener(VoxImplant.Events.IncomingCall, (e: VoxImplant.Events.IncomingCall) => this.voxIncomingCall(e));
165 | 		// Logs
166 | 		//this.voxAPI.writeLog = function(message) { }
167 | 		//this.voxAPI.writeTrace = function(message) { }
168 | 		// IM & Presence
169 | 		this.voxAPI.addEventListener(VoxImplant.IMEvents.UCConnected, (e: VoxImplant.IMEvents.UCConnected) => this.voxUCConnected(e));
170 | 		this.voxAPI.addEventListener(VoxImplant.IMEvents.RosterReceived, (e: VoxImplant.IMEvents.RosterReceived) => this.voxRosterReceived(e));
171 | 		this.voxAPI.addEventListener(VoxImplant.IMEvents.RosterPresenceUpdate, (e: VoxImplant.IMEvents.RosterPresenceUpdate) => this.voxRosterPresenceUpdate(e));
172 | 		this.voxAPI.addEventListener(VoxImplant.IMEvents.RosterItemChange, (e: VoxImplant.IMEvents.RosterItemChange) => this.voxRosterItemChange(e));
173 | 		// Init
174 | 		this.voxAPI.init({
175 | 			micRequired: true
176 | 		});
177 | 	}
178 | 
179 | 	voxReady(e: VoxImplant.Events.SDKReady) {
180 | 		console.log("VoxImplant WebSDK v. " + e.version + " ready");
181 | 		this.voxAPI.connect();
182 | 	}
183 | 
184 | 	voxConnected(e: VoxImplant.Events.ConnectionEstablished) {
185 | 		console.log("Connection established");			
186 | 		this.setState({
187 | 			view: AppViews.CONNECTED
188 | 		});
189 | 	}
190 | 
191 | 	voxUCConnected(e: VoxImplant.IMEvents.UCConnected) {
192 | 		console.log("UC connected");
193 | 	}
194 | 
195 | 	voxRosterReceived(e: VoxImplant.IMEvents.RosterReceived) {
196 | 		this.roster = e.roster;
197 | 		this.forceUpdate();
198 | 	}
199 | 
200 | 	voxRosterPresenceUpdate(e: VoxImplant.IMEvents.RosterPresenceUpdate) {
201 | 		let user: string = e.id.substr(0, e.id.indexOf('@'));
202 | 		if (e.presence == VoxImplant.UserStatuses.Offline) {
203 | 			delete (this.presence[user]);
204 | 
205 | 			let index: number = -1;
206 | 			for (let i = 0; i < this.participants.length; i++) {
207 | 				if (this.participants[i]["name"] == user) {
208 | 					index = i;
209 | 					break;
210 | 				}
211 | 			}
212 | 			if (index != -1) this.participants.splice(index, 1);
213 | 				
214 | 		} else {
215 | 			this.presence[e.id] = e.presence;
216 | 		}
217 | 
218 | 		this.forceUpdate();
219 | 	}
220 | 
221 | 	voxRosterItemChange(e: VoxImplant.IMEvents.RosterItemChange) {
222 | 		if (e.type == VoxImplant.RosterItemEvent.Added) {
223 | 			this.roster.push({
224 | 				groups: [this.appname+"."+this.accname+".voximplant.com"],
225 | 				id: e.id,
226 | 				name: e.displayName,
227 | 				resources: [],
228 | 				subscription_type: 8
229 | 			});
230 | 		} else if (e.type == VoxImplant.RosterItemEvent.Removed) {
231 | 			let user: string = e.id.substr(0, e.id.indexOf('@'));
232 | 			let index: number = -1;
233 | 			for (let i = 0; i < this.roster.length; i++) {
234 | 				if (this.roster[i].id == user) {
235 | 					index = i;
236 | 					break;
237 | 				}
238 | 			}
239 | 			if (index != -1) this.roster.splice(index, 1);
240 | 		}
241 | 	}
242 | 
243 | 	voxConnectionFailed(e: VoxImplant.Events.ConnectionFailed) {
244 | 		console.log("Connection failed");
245 | 		this.setState({
246 | 			view: AppViews.FINISHED
247 | 		});
248 | 	}
249 | 
250 | 	voxConnectionClosed(e: VoxImplant.Events.ConnectionClosed) {
251 | 		console.log("Connection closed");
252 | 		this.setState({
253 | 			view: AppViews.FINISHED
254 | 		});
255 | 	}
256 | 
257 | 	voxMicAccessResult(e: VoxImplant.Events.MicAccessResult) {
258 | 		console.log("Mic access " + (e.result ? "allowed" : "denied"));
259 | 		this.localStream = e.stream;
260 | 		this.setState({
261 | 			tip: "Establishing connection"
262 | 		});
263 | 	}
264 | 
265 | 	voxAuthEvent(e: VoxImplant.Events.AuthResult) {
266 | 		if (e.result) {
267 | 			this.displayName = e.displayName;
268 | 			this.setState({
269 | 				view: AppViews.CONFERENCE_PARTICIPANTS
270 | 			});
271 | 		} else {
272 | 			if (e.code == 302) {
273 | 				let uid = this.username + "@" + this.appname + "." + this.accname + ".voximplant.com";
274 | 				$.get(this.wsURL + '?key=' + e.key + '&username=' + this.username, function(data) {
275 | 					if (data != "NO_DATA") {
276 | 						this.voxAPI.loginWithOneTimeKey(uid, data);
277 | 					}
278 | 				}.bind(this));
279 | 			} else {
280 | 				console.log("auth failed");
281 | 			}
282 | 		}
283 | 	}
284 | 
285 | 	authorize(displayName: string) {
286 | 		this.displayName = displayName;
287 | 		this.setState({
288 | 			view: AppViews.AUTH,
289 | 			tip: "Authorizing"
290 | 		});
291 | 
292 | 		$.get(this.wsURL + '?action=JOIN_CONFERENCE&displayName=' + this.displayName, function(data) {
293 | 			try {
294 | 				let result = JSON.parse(data);
295 | 				if (typeof result.username != "undefined") {
296 | 					// Login
297 | 					console.log(result);
298 | 					this.username = result.username;
299 | 					this.voxAPI.requestOneTimeLoginKey(this.username + "@" + this.appname + "." + this.accname + ".voximplant.com");
300 | 				}
301 | 			} catch (e) {
302 | 				console.log(e);
303 | 			}
304 | 		}.bind(this));
305 | 
306 | 	}
307 | 
308 | 	startConference() {
309 | 		this.host = true;
310 | 		for (let i = 0; i < this.participants.length; i++) {
311 | 			this.participants[i]["status"] = CallStatuses.INIT;
312 | 			let call: VoxImplant.Call = this.voxAPI.call(this.participants[i]["name"], false, null, { "X-DirectCall": "true" });
313 | 			call.addEventListener(VoxImplant.CallEvents.Connected, (e: VoxImplant.CallEvents.Connected) => this.handleCallConnected(e));
314 | 			call.addEventListener(VoxImplant.CallEvents.Disconnected, (e: VoxImplant.CallEvents.Disconnected) => this.handleCallDisconnected(e));
315 | 			call.addEventListener(VoxImplant.CallEvents.Failed, (e: VoxImplant.CallEvents.Failed) => this.handleCallFailed(e));			
316 | 			this.peerCalls.push(call);
317 | 		}
318 | 		this.mixes[this.username] = new Mix(this.username, this.localStream, this.audioCtx, this.host);		
319 | 		this.setState({
320 | 			view: AppViews.CONFERENCE_CALLING
321 | 		});
322 | 	}
323 | 
324 | 	finishConference() {
325 | 		for (let i = 0; i < this.peerCalls.length; i++) {
326 | 			this.peerCalls[i].hangup();
327 | 		}
328 | 		this.setState({
329 | 			view: AppViews.FINISHED
330 | 		});
331 | 	}
332 | 
333 | 	handleCallConnected(e: VoxImplant.CallEvents.Connected) {
334 | 		this.voxAPI.setCallActive(e.call, true);
335 | 		for (let i = 0; i < this.participants.length; i++) {
336 | 			if (this.participants[i]["name"] == e.call.number()) this.participants[i]["status"] = CallStatuses.CONNECTED;
337 | 		}
338 | 		this.forceUpdate();
339 | 		// Remote stream doesn't appear immediately - waiting for it
340 | 		var ts = setInterval(() => {
341 | 
342 | 			if (e.call.getPeerConnection().getRemoteAudioStream() != null) {
343 | 
344 | 				//(document.getElementById(e.call.getAudioElementId()) as HTMLMediaElement).volume = 0;
345 | 				//console.log(e.call.number() + ": " + e.call.getAudioElementId());
346 | 
347 | 				clearInterval(ts);
348 | 				this.mixes[e.call.number()] = new Mix(e.call.number(), this.localStream, this.audioCtx);
349 | 
350 | 				for (let i = 0; i < this.peerCalls.length; i++) {
351 | 					if (this.peerCalls[i].number() != e.call.number()) {
352 | 						console.log("Attaching " + this.peerCalls[i].number() + " audio stream to " + e.call.number() + " mix");
353 | 						this.mixes[e.call.number()].addParticipant(this.peerCalls[i].number(), this.peerCalls[i].getPeerConnection().getRemoteAudioStream());
354 | 					}
355 | 				}					
356 | 
357 | 				e.call.getPeerConnection().setLocalStream(this.mixes[e.call.number()].getResultStream());				
358 | 
359 | 				for (let i = 0; i < this.participants.length; i++) {
360 | 					if (this.participants[i]["name"] == e.call.number()) this.participants[i]["status"] = CallStatuses.STREAM_CONNECTED;
361 | 				}
362 | 				this.forceUpdate();
363 | 
364 | 			}
365 | 
366 | 		}, 1000);
367 | 
368 | 	}
369 | 
370 | 	handleCallDisconnected(e: VoxImplant.CallEvents.Disconnected) {
371 | 		for (let i = 0; i < this.participants.length; i++) {
372 | 			if (this.participants[i]["name"] == e.call.number()) this.participants[i]["status"] = CallStatuses.ENDED;
373 | 		}
374 | 		let index: number = this.peerCalls.indexOf(e.call);
375 | 		if (index > -1) this.peerCalls.splice(index, 1);
376 | 		if (this.peerCalls.length > 0) this.forceUpdate();
377 | 		else {
378 | 			this.participants = [];
379 | 			this.setState({
380 | 				view: AppViews.CONFERENCE_PARTICIPANTS
381 | 			});
382 | 		}
383 | 	}
384 | 
385 | 	handleCallFailed(e: VoxImplant.CallEvents.Failed) {
386 | 		console.log("Call to " + e.call.number() + " failed");
387 | 		for (let i = 0; i < this.participants.length; i++) {
388 | 			if (this.participants[i]["name"] == e.call.number()) this.participants[i]["status"] = CallStatuses.ENDED;
389 | 		}
390 | 		let index: number = this.peerCalls.indexOf(e.call);
391 | 		if (index > -1) this.peerCalls.splice(index, 1);
392 | 		if (this.peerCalls.length > 0) this.forceUpdate();
393 | 		else this.setState({
394 | 			view: AppViews.FINISHED
395 | 		});
396 | 	}
397 | 
398 | 	voxIncomingCall(e: VoxImplant.Events.IncomingCall) {
399 | 		if (this.state.view == AppViews.INBOUND || this.state.view == AppViews.CONFERENCE_CALLING) e.call.reject();
400 | 		else {
401 | 			this.setState({
402 | 				view: AppViews.INBOUND
403 | 			})
404 | 			// No need to do anything special - all magic is on the host side
405 | 			this.peerCalls.push(e.call);
406 | 			e.call.addEventListener(VoxImplant.CallEvents.Disconnected, (e: VoxImplant.CallEvents.Disconnected) => this.handleCallDisconnected(e));
407 | 			e.call.answer();
408 | 		}
409 | 	}
410 | 
411 | 	mutePlayback() {
412 | 		console.log("Mute playback");
413 | 		for (let i = 0; i < this.peerCalls.length; i++ ) {
414 | 			this.peerCalls[i].mutePlayback();
415 | 		}
416 | 	}
417 | 
418 | 	unmutePlayback() {
419 | 		console.log("Unmute playback");
420 | 		for (let i = 0; i < this.peerCalls.length; i++) {
421 | 			this.peerCalls[i].unmutePlayback();
422 | 		}
423 | 	}
424 | 
425 | 	muteMic() {
426 | 		console.log("Mute microphone");
427 | 		for (let i = 0; i < this.peerCalls.length; i++) {
428 | 			this.peerCalls[i].muteMicrophone();
429 | 		}
430 | 	}
431 | 
432 | 	unmuteMic() {
433 | 		console.log("Unmute microphone");
434 | 		for (let i = 0; i < this.peerCalls.length; i++) {
435 | 			this.peerCalls[i].unmuteMicrophone();
436 | 		}
437 | 	}
438 | 
439 | 	onListItemClick(e: string) {
440 | 
441 | 		let index: number = -1;
442 | 		for (let i = 0; i < this.participants.length; i++) {
443 | 			if (this.participants[i]["name"] == e.substr(0, e.indexOf('@'))) {
444 | 				index = i;
445 | 				break;
446 | 			}
447 | 		}
448 | 
449 | 		if (index != -1) this.participants.splice(index, 1);
450 | 		else this.participants.push({ name: e.substr(0, e.indexOf('@')) });
451 | 		this.forceUpdate();
452 | 	}
453 | 
454 | 	render() {
455 | 		if (this.state.view == AppViews.INIT || this.state.view == AppViews.AUTH) {
456 | 
457 | 			return 
458 | 				
{this.state.tip}
459 | 				
464 | 			
 this.startConference() }>Start ;
475 | 			
476 | 			let online_users: number = 0;
477 | 			for (var i in this.presence) {
478 | 				if (this.presence[i] == VoxImplant.UserStatuses.Online) {
479 | 					online_users++;
480 | 				}
481 | 			}
482 | 			if (online_users > 0) msg = "Choose users to start the conference or wait until someone call you";
483 | 			else msg = "Nobody is online at the moment";
484 | 
485 | 			/**
486 | 			* 	 this.mutePlayback() }>Mute Playback 
487 | 			*	 this.unmutePlayback() }>Unmute Playback 
488 | 			*	 this.muteMic() }>Mute Mic 
489 | 			*	 this.unmuteMic() }>Unmute Mic 
490 | 			*/
491 | 
492 | 			return 
493 | 				
494 | 					
495 | 						Client-side Audio Conference 
496 | 					 
497 | 
498 | 					
499 | 						Online Users 
500 | 						{msg}
501 | 						
502 | 							{this.roster.map(function(obj) {
503 | 
504 | 								if (typeof this.presence[obj["id"]] != "undefined" &&
505 | 									this.presence[obj["id"]] == VoxImplant.UserStatuses.Online) {
506 | 
507 | 									let found: boolean = false;
508 | 									for (let i = 0; i < this.participants.length; i++) {
509 | 										if (this.participants[i]["name"] == obj["id"].substr(0, obj["id"].indexOf('@'))) {
510 | 											found = true;
511 | 											break;
512 | 										}
513 | 									}
514 | 									
515 | 									if (found) return  this.onListItemClick(obj["id"]) } active>{obj["name"]} ;
516 | 									else return  this.onListItemClick(obj["id"]) }>{obj["name"]} ;
517 | 
518 | 								}
519 | 
520 | 							}.bind(this)) }
521 | 						 
522 | 					 
523 | 
524 | 					
525 | 						{button}
526 | 					 
527 | 
528 | 				 
529 | 			
 this.finishConference() }>Finish ;
535 | 
536 | 			let processed_number: number = 0;
537 | 			for (let i = 0; i < this.participants.length; i++) {
538 | 				if (this.participants[i]["status"] == CallStatuses.ENDED ||
539 | 					this.participants[i]["status"] == CallStatuses.STREAM_CONNECTED) processed_number++;
540 | 			}
541 | 			if (processed_number == this.participants.length) msg = "";
542 | 
543 | 			return 
544 | 				
545 | 					
546 | 						Client-side Audio Conference 
547 | 					 
548 | 
549 | 					
550 | 						{processed_number != this.participants.length?"Creating conference":"Conference is live"} 
551 | 						{msg}
552 | 						
553 | 							{this.participants.map(function(obj) {
554 | 
555 | 								let style: string,
556 | 									name: string = "";
557 | 
558 | 								switch (obj["status"]) {
559 | 									case CallStatuses.INIT:
560 | 										style = "info";
561 | 										break;
562 | 
563 | 									case CallStatuses.CONNECTED:
564 | 										style = "warning";
565 | 										break;
566 | 
567 | 									case CallStatuses.STREAM_CONNECTED:
568 | 										style = "success";
569 | 										break;
570 | 
571 | 									case CallStatuses.ENDED:
572 | 										style = "danger";
573 | 										break;
574 | 								}
575 | 
576 | 								for (let i = 0; i < this.roster.length; i++) {
577 | 									if (this.roster[i].id == obj["name"]+"@"+this.appname+"."+this.accname+".voximplant.com") {
578 | 										name = this.roster[i].name;
579 | 										break;
580 | 									}
581 | 								}
582 | 								console.log(obj);
583 | 								return {name} ;
584 | 							}.bind(this)) }
585 | 						 
586 | 					 
587 | 
588 | 					
589 | 						{button}
590 | 					 
591 | 				 
592 | 			
 this.finishConference() }>Finish ;
597 | 
598 | 			return 
599 | 				
600 | 					
601 | 						Client-side Audio Conference 
602 | 					 
603 | 
604 | 					
605 | 						Connected to the conference
606 | 					 
607 | 
608 | 					
609 | 						{button}
610 | 					 
611 | 
612 | 				 
613 | 			
;
614 | 
615 | 		} else {
616 | 
617 | 			return Thank you!
;
618 | 		}
619 | 	}
620 | }
621 | 
622 | export default App;
623 | 
624 | ReactDOM.render(